pkg/httpx => github.com/tkw1536/pkglib/httpx
This commit is contained in:
parent
5e89fadeeb
commit
010fd536ea
35 changed files with 37 additions and 1081 deletions
|
|
@ -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
|
||||
})
|
||||
)
|
||||
|
|
@ -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"
|
||||
)
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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)))
|
||||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue