pkg/httpx => github.com/tkw1536/pkglib/httpx

This commit is contained in:
Tom Wiesing 2023-02-26 10:08:17 +01:00
parent 5e89fadeeb
commit 010fd536ea
No known key found for this signature in database
35 changed files with 37 additions and 1081 deletions

View file

@ -1,81 +0,0 @@
package httpx
import (
"encoding/json"
"errors"
"net/http"
)
// ErrInterceptor intercepts errors and directly returns specific responses for them
type ErrInterceptor struct {
Errors map[error]Response
Fallback Response
}
// Intercept attempts to intercept the given error.
// When err is nil, does nothing.
//
// When err is not nil, first attempts to find a static response in errors and respond with that.
// Otherwise it returns the Fallback response.
// intercepted indicates if some response was sent.
func (ei ErrInterceptor) Intercept(w http.ResponseWriter, r *http.Request, err error) (intercepted bool) {
if err == nil {
return false
}
res, ok := ei.Errors[err]
if !ok {
res = ei.Fallback
}
res.ServeHTTP(w, r)
return true
}
// StatusInterceptor creates a new ErrInterceptor handling default responses.
// If body returns err != nil, StatusInterceptor calls panic().
func StatusInterceptor(contentType string, body func(code int, text string) ([]byte, error)) ErrInterceptor {
makeResponse := func(code int) (res Response) {
var err error
res.Body, err = body(code, http.StatusText(code))
if err != nil {
panic("StatusInterceptor: err != nil")
}
res.ContentType = contentType
res.StatusCode = code
return
}
return ErrInterceptor{
Errors: map[error]Response{
ErrInternalServerError: makeResponse(http.StatusInternalServerError),
ErrBadRequest: makeResponse(http.StatusBadRequest),
ErrNotFound: makeResponse(http.StatusNotFound),
ErrForbidden: makeResponse(http.StatusForbidden),
ErrMethodNotAllowed: makeResponse(http.StatusMethodNotAllowed),
},
Fallback: makeResponse(http.StatusInternalServerError),
}
}
// Common errors accepted by all httpx handlers
var (
ErrInternalServerError = errors.New("httpx: Internal Server Error")
ErrBadRequest = errors.New("httpx: Bad Request")
ErrNotFound = errors.New("httpx: Not Found")
ErrForbidden = errors.New("httpx: Forbidden")
ErrMethodNotAllowed = errors.New("httpx: Method Not Allowed")
)
var (
TextInterceptor = StatusInterceptor("text/plain", func(code int, text string) ([]byte, error) {
return []byte(text), nil
})
JSONInterceptor = StatusInterceptor("application/json", func(code int, text string) ([]byte, error) {
return json.Marshal(map[string]any{"status": text, "code": code})
})
HTMLInterceptor = StatusInterceptor("text/html", func(code int, text string) ([]byte, error) {
return MinifyHTML([]byte(`<!DOCTYPE HTML><title>` + text + `</title>` + text)), nil
})
)

View file

@ -1,63 +0,0 @@
package field
// Autocomplete represents different autocomplete options
type Autocomplete string
const (
Off Autocomplete = "off"
On Autocomplete = "on"
Name Autocomplete = "name"
HonorificPrefix Autocomplete = "honorific-prefix"
GivenName Autocomplete = "given-name"
AdditionalName Autocomplete = "additional-name"
FamilyName Autocomplete = "family-name"
HonorificSuffix Autocomplete = "honorific-suffix"
Nickname Autocomplete = "nickname"
Email_ Autocomplete = "email"
Username Autocomplete = "username"
NewPassword Autocomplete = "new-password"
CurrentPassword Autocomplete = "current-password"
OneTimeCode Autocomplete = "one-time-code"
OrganizationTitle Autocomplete = "organization-title"
Organization Autocomplete = "organization"
AddressLine1 Autocomplete = "address-line1"
AddressLine2 Autocomplete = "address-line2"
AddressLine3 Autocomplete = "address-line3"
StreetAddress Autocomplete = "street-address"
AddressLevel4 Autocomplete = "address-level4"
AddressLevel3 Autocomplete = "address-level3"
AddressLevel2 Autocomplete = "address-level2"
AddressLevel1 Autocomplete = "address-level1"
Country Autocomplete = "country"
CountryName Autocomplete = "country-name"
PostalCode Autocomplete = "postal-code"
CcName Autocomplete = "cc-name"
CcGivenName Autocomplete = "cc-given-name"
CcAdditionalName Autocomplete = "cc-additional-name"
CcFamilyName Autocomplete = "cc-family-name"
CcNumber Autocomplete = "cc-number"
CcExp Autocomplete = "cc-exp"
CcExpMonth Autocomplete = "cc-exp-month"
CcExpYear Autocomplete = "cc-exp-year"
CcCsc Autocomplete = "cc-csc"
CcType Autocomplete = "cc-type"
TransactionCurrency Autocomplete = "transaction-currency"
TransactionAmount Autocomplete = "transaction-amount"
Language Autocomplete = "language"
Bday Autocomplete = "bday"
BdayDay Autocomplete = "bday-day"
BdayMonth Autocomplete = "bday-month"
BdayYear Autocomplete = "bday-year"
Sex Autocomplete = "sex"
Tel_ Autocomplete = "tel"
TelCountryCode Autocomplete = "tel-country-code"
TelNational Autocomplete = "tel-national"
TelAreaCode Autocomplete = "tel-area-code"
TelLocal Autocomplete = "tel-local"
TelLocalPrefix Autocomplete = "tel-local-prefix"
TelLocalSuffix Autocomplete = "tel-local-suffix"
TelExtension Autocomplete = "tel-extension"
Impp Autocomplete = "impp"
Url_ Autocomplete = "url"
Photo Autocomplete = "photo"
)

View file

@ -1,54 +0,0 @@
package field
import (
"html/template"
"io"
)
// DefaultFieldTemplate is the default template to render fields.
var DefaultFieldTemplate = template.Must(template.New("").Parse(`
{{ if (eq .Type "textarea" )}}
<textarea name="{{.Name}}" id="{{.Name}}" placeholder="{{.Placeholder}}"{{if .Autocomplete }} autocomplete="{{.Autocomplete}}" {{end}}>{{.Value}}</textarea>
{{ else }}
<input type="{{.Type}}" value="{{.Value}}" name="{{.Name}}" placeholder={{.Placeholder}}{{if .Autocomplete }} autocomplete="{{.Autocomplete}}{{end}}>
{{ end }}`))
var PureCSSFieldTemplate = template.Must(template.New("").Parse(`
<div class="pure-control-group">
<label for="{{.Name}}">{{.Label}}</label>
{{ if (eq .Type "textarea" )}}
<textarea name="{{.Name}}" id="{{.Name}}" placeholder="{{.Placeholder}}"{{if .Autocomplete }} autocomplete="{{.Autocomplete}}" {{end}}>{{.Value}}</textarea>
{{ else }}
<input type="{{.Type}}" value="{{.Value}}" name="{{.Name}}" id="{{.Name}}" placeholder="{{.Placeholder}}"{{if .Autocomplete }} autocomplete="{{.Autocomplete}}" {{end}}>
{{ end }}
</div>`))
// Field represents a field inside a form.
type Field struct {
Name string // Name is the name of the field
Type InputType // Type is the type of the field. It corresponds to the "name" attribute in html.
Placeholder string // Value for the "placeholder" attribute
Label string // (External) Label for the field. Not used by the default template.
Autocomplete Autocomplete
EmptyOnError bool // indicates if the field should be reset on error
}
// fieldContext is passed to the template context
type fieldContext struct {
Field
Value string
}
func (field Field) WriteTo(w io.Writer, template *template.Template, value string) {
if template == nil {
template = DefaultFieldTemplate
}
template.Execute(w, fieldContext{Field: field, Value: value})
}
// CheckboxChecked is the default value of a checked checkbox
const CheckboxChecked = "on"

View file

@ -1,32 +0,0 @@
package field
// InputType represents the type of input
type InputType string
const (
Button InputType = "button"
Checkbox InputType = "checkbox"
Color InputType = "color"
Date InputType = "date"
DatetimeLocal InputType = "datetime-local"
Email InputType = "email"
File InputType = "file"
Hidden InputType = "hidden"
Image InputType = "image"
Month InputType = "month"
Number InputType = "number"
Password InputType = "password"
Radio InputType = "radio"
Range InputType = "range"
Reset InputType = "reset"
Search InputType = "search"
Submit InputType = "submit"
Tel InputType = "tel"
Text InputType = "text"
Time InputType = "time"
Url InputType = "url"
Week InputType = "week"
Datetime InputType = "datetime"
Textarea InputType = "textarea" // special
)

View file

@ -1,170 +0,0 @@
package httpx
import (
"html/template"
"net/http"
_ "embed"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx/field"
"github.com/gorilla/csrf"
"github.com/tkw1536/pkglib/pools"
)
// Form provides a form that a user can submit via a http POST method call.
// It implements [http.Handler].
type Form[D any] struct {
// Fields are the fields this form consists of.
Fields []field.Field
// FieldTemplate is an optional template to be executed for each field.
// FieldTemplate may be nil; in which case [DefaultFieldTemplate] is used.
FieldTemplate *template.Template
// SkipCSRF if CSRF should be explicitly omitted
SkipCSRF bool
// SkipForm, if non-nil, is called on every get request to determine if form parsing should be skipped entirely.
// If skip is true, RenderSuccess is directly called with the given values map.
SkipForm func(r *http.Request) (data D, skip bool)
// RenderTemplate represents the template to render for GET requests.
// It is passed the return value of [RenderTemplateContext], or a [FormContext] instance if this does not exist.
RenderTemplate *template.Template
// RenderTemplateContext is the context to be used for RenderTemplate.
// When nil, assumed to be the identify function
RenderTemplateContext func(ctx FormContext, r *http.Request) any
// Validate, if non-nil, validates the given submitted values.
// There is no guarantee that the values are set.
Validate func(r *http.Request, values map[string]string) (D, error)
// RenderSuccess handles rendering a success result into a response.
RenderSuccess func(data D, values map[string]string, w http.ResponseWriter, r *http.Request) error
}
// Form renders the gives values into a template html string to be inserted into a template.
func (form *Form[D]) Form(values map[string]string, isError bool) template.HTML {
builder := pools.GetBuilder()
defer pools.ReleaseBuilder(builder)
for _, field := range form.Fields {
value := values[field.Name]
if isError && field.EmptyOnError {
value = ""
}
field.WriteTo(builder, form.FieldTemplate, value)
}
return template.HTML(builder.String())
}
// Values returns (validated) form values contained in the given request
func (form *Form[D]) Values(r *http.Request) (v map[string]string, d D, err error) {
// parse the form
if err := r.ParseForm(); err != nil {
return nil, d, err
}
// pick each of the values
values := make(map[string]string, len(form.Fields))
for _, field := range form.Fields {
values[field.Name] = r.PostForm.Get(field.Name)
}
// validate the form
if form.Validate != nil {
d, err = form.Validate(r, values)
if err != nil {
return nil, d, err
}
}
// and return them
return values, d, nil
}
// ServeHTTP implements [http.Handler] and serves the form
func (form *Form[D]) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch {
default:
TextInterceptor.Intercept(w, r, ErrMethodNotAllowed)
return
case r.Method == http.MethodPost:
values, data, err := form.Values(r)
if err != nil {
form.renderForm(err, values, w, r)
} else {
form.renderSuccess(data, values, w, r)
}
case r.Method == http.MethodGet && form.SkipForm != nil:
if data, skip := form.SkipForm(r); skip {
form.renderSuccess(data, nil, w, r)
return
}
fallthrough
case r.Method == http.MethodGet:
form.renderForm(nil, nil, w, r)
}
}
// renderForm renders the form onto the request
func (form *Form[D]) renderForm(err error, values map[string]string, w http.ResponseWriter, r *http.Request) {
template := form.Form(values, err != nil)
if !form.SkipCSRF {
template += csrf.TemplateField(r)
}
ctx := FormContext{Err: err, Form: template}
// must have a form or a RenderForm
if form.RenderTemplate == nil {
panic("form.RenderTemplate is nil")
}
// get the template context
var tplctx any
if form.RenderTemplateContext == nil {
tplctx = ctx
} else {
tplctx = form.RenderTemplateContext(ctx, r)
}
// render the form
WriteHTML(tplctx, nil, form.RenderTemplate, "", w, r)
}
// FormContext is passed to Form.Form when used
type FormContext struct {
// Error is the underlying error (if any)
Err error
// Template is the underlying template rendered as html
Form template.HTML
}
// Error returns the underlying error string
func (fc FormContext) Error() string {
if fc.Err == nil {
return ""
}
return fc.Err.Error()
}
// renderSuccess renders a successfull pass of the form
// if an error occurs during rendering, renderForm is called instead
func (form *Form[D]) renderSuccess(data D, values map[string]string, w http.ResponseWriter, r *http.Request) {
err := form.RenderSuccess(data, values, w, r)
if err == nil {
return
}
form.renderForm(err, values, w, r)
}
//go:embed "form.html"
var formBytes []byte
// FormTeplate is a template to embed a form
var FormTemplate = template.Must(template.New("form.html").Parse(string(formBytes)))

View file

@ -1,22 +0,0 @@
<div class="pure-u-1">
{{ block "form/extra" . }}<!-- no extra -->{{ end }}
<form class="pure-form pure-form-aligned" method="POST">
<fieldset>
{{ block "form/message" . }}
{{ $E := .Error }}
{{ if not (eq $E "") }}
<div class="pure-form-group">
<p class="error-message">
{{ $E }}
</p>
</div>
{{ end }}
{{ end }}
{{ block "form/inside" . }}<!-- no inside -->{{ end }}
{{ .Form }}
<input type="submit" value="{{ block "form/button" .}}Submit{{ end }}" class="pure-button">
</fieldset>
</form>
</div>

View file

@ -1,79 +0,0 @@
package httpx
import (
"html/template"
"net/http"
"time"
"github.com/rs/zerolog"
"github.com/tkw1536/pkglib/timex"
)
const HTMLFlushInterval = time.Second / 10
// WriteHTML writes a html response of type T to w.
// If an error occured, writes an error response instead.
func WriteHTML[T any](result T, err error, template *template.Template, templateName string, w http.ResponseWriter, r *http.Request) (e error) {
// log any error that occurs;
defer func() {
if e != nil {
zerolog.Ctx(r.Context()).Err(e).Str("path", r.URL.String()).Msg("error rendering template")
}
}()
// create a synced respone writer
sw := &SyncedResponseWriter{ResponseWriter: w}
done := make(chan struct{})
defer close(done)
// and regularly flush it until the end of the function
go func() {
timer := timex.NewTimer()
defer timex.ReleaseTimer(timer)
for {
timer.Reset(HTMLFlushInterval)
select {
case <-timer.C:
sw.Flush()
case <-done:
return
}
}
}()
// intercept any errors
if HTMLInterceptor.Intercept(sw, r, err) {
return nil
}
// write out the response as html
sw.Header().Set("Content-Type", "text/html")
sw.WriteHeader(http.StatusOK)
// minify html!
minifier := MinifyHTMLWriter(sw)
defer minifier.Close()
// and return the template
if templateName != "" {
return template.ExecuteTemplate(minifier, templateName, result)
} else {
return template.Execute(minifier, result)
}
}
type HTMLHandler[T any] struct {
Handler func(r *http.Request) (T, error)
Template *template.Template // called with T
TemplateName string // name of template to render, defaults to root
}
// ServeHTTP calls j(r) and returns json
func (h HTMLHandler[T]) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// call the function
result, err := h.Handler(r)
WriteHTML(result, err, h.Template, h.TemplateName, w, r)
}

View file

@ -1,54 +0,0 @@
//go:build !nominify
package httpx
import (
"io"
"regexp"
"github.com/tdewolff/minify"
"github.com/tdewolff/minify/css"
"github.com/tdewolff/minify/html"
"github.com/tdewolff/minify/js"
"github.com/tdewolff/minify/svg"
)
// minifier holds the minfier used for all html minification
//
// NOTE(twiesing): We can't use an init function for this, because otherwise initialization order is incorrect.
var minifier = (func() *minify.M {
m := minify.New()
m.AddFunc("text/html", html.Minify)
m.AddFunc("text/css", css.Minify)
m.AddFunc("image/svg+xml", svg.Minify)
m.AddFuncRegexp(regexp.MustCompile("^(application|text)/(x-)?(java|ecma)script$"), js.Minify)
return m
})()
// MinifyHTMLWriter wraps the given io.Writer to minify the given html instead.
// The writer must be closed explicitly.
//
// Specific environments may chose to disable http minification, in which case MinifyHTMLWriter becomes the identity function.
func MinifyHTMLWriter(dest io.Writer) io.WriteCloser {
return minifier.Writer("text/html", dest)
}
// MinifyHTML minifies the html source.
// If an error occurs, returns the unmodified source instead.
func MinifyHTML(source []byte) []byte {
result, err := minifier.Bytes("text/html", source)
if err != nil {
return source
}
return result
}
// MinifySVG minifies the svg source.
// If an error occurs, returns the minified source instead.
func MinifySVG(source []byte) []byte {
result, err := minifier.Bytes("image/svg+xml", source)
if err != nil {
return source
}
return result
}

View file

@ -1,31 +0,0 @@
//go:build nominify
package httpx
import "io"
// MinifyHTMLWriter wraps the given io.Writer to minify the given html instead.
// The writer must be closed explicitly.
//
// Specific environments may chose to disable http minification, in which case MinifyHTMLWriter becomes the identity function.
func MinifyHTMLWriter(dest io.Writer) io.WriteCloser {
return noop{Writer: dest}
}
type noop struct {
io.Writer
}
func (noop) Close() error {
return nil
}
// MinifyHTML minifies the html source.
// If an error occurs, returns the unmodified source instead.
func MinifyHTML(source []byte) []byte {
return source
}
func MinifySVG(source []byte) []byte {
return source
}

View file

@ -1,27 +0,0 @@
package httpx
import (
"net/http"
"strconv"
)
// Response represents a response to an http request.
type Response struct {
ContentType string // defaults to text/plain
Body []byte
StatusCode int // defaults to [http.StatusOK]
}
func (response Response) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if response.ContentType == "" {
response.ContentType = "text/plain"
}
w.Header().Set("Content-Type", response.ContentType)
w.Header().Set("Content-Length", strconv.Itoa(len(response.Body)))
if response.StatusCode <= 0 {
response.StatusCode = http.StatusOK
}
w.WriteHeader(response.StatusCode)
w.Write(response.Body)
}

View file

@ -1,35 +0,0 @@
package httpx
import (
"encoding/json"
"net/http"
)
// JSON creates a new JSONHandler
func JSON[T any](f func(r *http.Request) (T, error)) JSONHandler[T] {
return JSONHandler[T](f)
}
// WriteJSON writes a JSON response of type T to w.
// If an error occured, writes an error response instead.
func WriteJSON[T any](result T, err error, w http.ResponseWriter, r *http.Request) {
// handle any errors
if JSONInterceptor.Intercept(w, r, err) {
return
}
// write out the response as json
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(result)
}
// JSONHandler implements [http.Handler] by returning values as json to the caller.
// In case of an error, a generic "internal server error" message is returned.
type JSONHandler[T any] func(r *http.Request) (T, error)
// ServeHTTP calls j(r) and returns json
func (j JSONHandler[T]) ServeHTTP(w http.ResponseWriter, r *http.Request) {
result, err := j(r)
WriteJSON(result, err, w, r)
}

View file

@ -1,22 +0,0 @@
package httpx
import (
"net/http"
)
// RedirectHandler represents a handler that redirects the user to the address returned
type RedirectHandler func(r *http.Request) (string, int, error)
// ServeHTTP calls r(r) and returns json
func (rh RedirectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// call the function
url, code, err := rh(r)
// intercept the errors
if TextInterceptor.Intercept(w, r, err) {
return
}
// do the redirect
http.Redirect(w, r, url, code)
}

View file

@ -1,316 +0,0 @@
package httpx
import (
"context"
"net/http"
"sync"
"time"
"github.com/gorilla/websocket"
"github.com/tkw1536/pkglib/lazy"
)
// WebSocket implements serving a WebSocket
type WebSocket struct {
Context context.Context // context which closes all connections
Limits WebSocketLimits // limits for websocket operations
Handler func(ws WebSocketConnection)
Fallback http.Handler
pool lazy.Lazy[*sync.Pool] // pool holds *WebSocketConn objects
upgrader websocket.Upgrader // upgrades upgrades connections
}
type WebSocketLimits struct {
WriteWait time.Duration // maximum time to wait for writing
PongWait time.Duration // time to wait for pong responses
PingInterval time.Duration // interval to send pings to the client
MaxMessageSize int64 // maximal message size in bytes
}
func (limits *WebSocketLimits) SetDefaults() {
if limits.WriteWait == 0 {
limits.WriteWait = 10 * time.Second
}
if limits.PongWait == 0 {
limits.PongWait = time.Minute
}
if limits.PingInterval <= 0 {
limits.PingInterval = (limits.PongWait * 9) / 10
}
if limits.MaxMessageSize <= 0 {
limits.MaxMessageSize = 2048
}
}
// makePoolSocket creates a new socket and makes sure that the pool is initialized
func (h *WebSocket) makePoolSocket() *webSocketConn {
return h.pool.Get(func() *sync.Pool {
return &sync.Pool{
New: func() any { return new(webSocketConn) },
}
}).Get().(*webSocketConn)
}
func (h *WebSocket) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// if the user did not request a websocket, go to the fallbacjk handler
if !websocket.IsWebSocketUpgrade(r) {
h.serveFallback(w, r)
return
}
// else deal with the websocket!
h.serveWebsocket(w, r)
}
func (h *WebSocket) serveFallback(w http.ResponseWriter, r *http.Request) {
if h.Fallback == nil {
http.NotFound(w, r)
return
}
h.Fallback.ServeHTTP(w, r)
}
func (h *WebSocket) serveWebsocket(w http.ResponseWriter, r *http.Request) {
// upgrade the connection or bail out!
conn, err := h.upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
// get a new socket from the pool
socket := h.makePoolSocket()
socket.Serve(h.Context, h.Limits, conn, h.Handler)
// return a reset socket to the pool
socket.reset()
h.pool.Get(nil).Put(socket)
}
// WebSocketConnection represents a connected WebSocket.
type WebSocketConnection interface {
// Context returns a context that is closed once the connection is terminated.
Context() context.Context
// Read returns a channel that receives message.
// The channel is closed if no more messags are available (for instance because the server closed).
Read() <-chan WebSocketMessage
// Write queues the provided message for sending.
// The returned channel is closed once the message has been sent.
Write(WebSocketMessage) <-chan struct{}
// WriteText is a convenience method to send a TextMessage.
// The returned channel is closed once the message has been sent.
WriteText(text string) <-chan struct{}
// Close closes the underlying connection
Close()
}
// WebSocketMessage represents a connected Websocket
type WebSocketMessage struct {
Type int
Bytes []byte
}
type outWebSocketMessage struct {
WebSocketMessage
done chan<- struct{} // done should be closed when finished
}
// webSocketConn implements [WebSocketConnection]
type webSocketConn struct {
conn *websocket.Conn // underlying connection
limits WebSocketLimits
context context.Context // context to cancel the connection
cancel context.CancelFunc
wg sync.WaitGroup // blocks all the ongoing tasks
// incoming and outgoing tasks
incoming chan WebSocketMessage
outgoing chan outWebSocketMessage
}
// Serve serves the provided connection
func (h *webSocketConn) Serve(ctx context.Context, limits WebSocketLimits, conn *websocket.Conn, handler func(ws WebSocketConnection)) {
// use the connection!
h.conn = conn
// setup limits
h.limits = limits
h.limits.SetDefaults()
// create a context for the connection
if ctx == nil {
ctx = context.Background()
}
h.context, h.cancel = context.WithCancel(ctx)
// start receiving and sending messages
h.wg.Add(2)
h.sendMessages()
h.recvMessages()
// wait for the context to be cancelled, then close the connection
h.wg.Add(1)
go func() {
defer h.wg.Done()
<-h.context.Done()
h.conn.Close()
}()
// start the application logic
h.wg.Add(1)
go h.handle(handler)
// wait for closing operations
h.wg.Wait()
}
func (h *webSocketConn) handle(handler func(ws WebSocketConnection)) {
defer func() {
h.wg.Done()
h.cancel()
}()
handler(h)
}
func (h *webSocketConn) sendMessages() {
h.outgoing = make(chan outWebSocketMessage)
go func() {
// close connection when done!
defer func() {
h.wg.Done()
h.cancel()
}()
// setup a timer for pings!
ticker := time.NewTicker(h.limits.PingInterval)
defer ticker.Stop()
for {
select {
// everything is done!
case <-h.context.Done():
return
// send outgoing messages
case message := <-h.outgoing:
(func() {
defer close(message.done)
err := h.writeRaw(message.Type, message.Bytes)
if err != nil {
return
}
message.done <- struct{}{}
})()
// send a ping message
case <-ticker.C:
if err := h.writeRaw(websocket.PingMessage, []byte{}); err != nil {
return
}
}
}
}()
}
// writeRaw writes to the underlying socket
func (h *webSocketConn) writeRaw(messageType int, data []byte) error {
h.conn.SetWriteDeadline(time.Now().Add(h.limits.WriteWait))
return h.conn.WriteMessage(messageType, data)
}
// Write writes a message to the websocket connection.
func (sh *webSocketConn) Write(message WebSocketMessage) <-chan struct{} {
callback := make(chan struct{}, 1)
go func() {
select {
// write an outgoing message
case sh.outgoing <- outWebSocketMessage{
WebSocketMessage: message,
done: callback,
}:
// context
case <-sh.context.Done():
close(callback)
}
}()
return callback
}
func (sh *webSocketConn) WriteText(text string) <-chan struct{} {
return sh.Write(WebSocketMessage{
Type: websocket.TextMessage,
Bytes: []byte(text),
})
}
func (h *webSocketConn) recvMessages() {
h.incoming = make(chan WebSocketMessage)
// set a read handler
h.conn.SetReadLimit(h.limits.MaxMessageSize)
// configure a pong handler
h.conn.SetReadDeadline(time.Now().Add(h.limits.PongWait))
h.conn.SetPongHandler(func(string) error { h.conn.SetReadDeadline(time.Now().Add(h.limits.PongWait)); return nil })
// handle incoming messages
go func() {
// close connection when done!
defer func() {
h.wg.Done()
h.cancel()
}()
for {
messageType, messageBytes, err := h.conn.ReadMessage()
if err != nil {
return
}
// try to send a message to the incoming message channel
select {
case h.incoming <- WebSocketMessage{
Type: messageType,
Bytes: messageBytes,
}:
case <-h.context.Done():
return
}
}
}()
}
// Read returns a channel that receives incoming messages.
// The channel is close once no more messages are available, or the context is canceled.
func (h *webSocketConn) Read() <-chan WebSocketMessage {
return h.incoming
}
// Context returns a context that is closed once this connection is closed.
func (h *webSocketConn) Context() context.Context {
return h.context
}
func (h *webSocketConn) Close() {
h.cancel()
}
// reset resets this websocket
func (h *webSocketConn) reset() {
h.limits = WebSocketLimits{}
h.conn = nil
h.incoming = nil
h.outgoing = nil
h.context, h.cancel = nil, nil
}

View file

@ -1,47 +0,0 @@
package httpx
import (
"net/http"
"sync"
)
// SyncedResponseWriter wraps a http ResponseWriter to syncronize all actions
type SyncedResponseWriter struct {
m sync.Mutex
http.ResponseWriter
}
func (rw *SyncedResponseWriter) Header() http.Header {
rw.m.Lock()
defer rw.m.Unlock()
return rw.ResponseWriter.Header()
}
func (rw *SyncedResponseWriter) Write(data []byte) (int, error) {
rw.m.Lock()
defer rw.m.Unlock()
return rw.ResponseWriter.Write(data)
}
func (rw *SyncedResponseWriter) WriteHeader(statusCode int) {
rw.m.Lock()
defer rw.m.Unlock()
rw.ResponseWriter.WriteHeader(statusCode)
}
// Flush flushes any partial output to the underlying ResponseWriter.
// If the wrapped ResponseWriter does not implement flush, the function performs no operation.
func (rw *SyncedResponseWriter) Flush() {
f, ok := rw.ResponseWriter.(http.Flusher)
if !ok {
return
}
rw.m.Lock()
defer rw.m.Unlock()
f.Flush()
}

View file

@ -1,14 +0,0 @@
package httpx
import (
"context"
"net/http"
)
// WithContextWrapper generates a new handler that wraps the context of each request with the wrapper function.
func WithContextWrapper(handler http.Handler, wrapper func(context.Context) context.Context) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r = r.WithContext(wrapper(r.Context()))
handler.ServeHTTP(w, r)
})
}