Compare commits

...

4 Commits

Author SHA1 Message Date
9426ab2bae implement channel API (no use yet) 2025-05-26 14:09:01 -04:00
5782177c35 update required env vars 2025-05-25 13:44:51 -04:00
b7a81889a3 paramaterize CORS 2025-05-25 13:43:52 -04:00
04c83cccb9 add provisional CORS support 2025-05-25 13:00:29 -04:00
12 changed files with 898 additions and 1 deletions

View File

@@ -5,10 +5,13 @@ import (
"fmt"
"log/slog"
"net/http"
"os"
"strings"
"git.dubyatp.xyz/chat-api-server/db"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
"github.com/go-chi/docgen"
"github.com/go-chi/render"
)
@@ -34,6 +37,15 @@ func Start() {
r := chi.NewRouter()
r.Use(cors.Handler(cors.Options{
AllowedOrigins: strings.Split(os.Getenv("ALLOWED_ORIGINS"), ","),
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
ExposedHeaders: []string{"Link"},
AllowCredentials: true,
MaxAge: 300, // Maximum value for preflight request cache
}))
r.Use(middleware.RequestID)
r.Use(RequestLog)
r.Use(middleware.Recoverer)
@@ -58,6 +70,22 @@ func Start() {
r.Get("/", Whoami)
})
r.Route("/channels", func(r chi.Router) {
r.Use(SessionAuthMiddleware)
r.Get("/", ListChannels)
r.Route("/{channelID}", func(r chi.Router) {
r.Use(ChannelCtx) // Load channel
r.Get("/", GetChannel)
r.Delete("/", DeleteChannel)
r.Post("/edit", EditChannel)
})
r.Route("/new", func(r chi.Router) {
r.Use(LoginCtx)
r.Post("/", NewChannel)
})
})
r.Route("/messages", func(r chi.Router) {
r.Use(SessionAuthMiddleware) // Protect with authentication

215
api/channel.go Normal file
View File

@@ -0,0 +1,215 @@
package api
import (
"context"
"encoding/json"
"log/slog"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/google/uuid"
)
func ChannelCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
slog.Debug("channel: entering ChannelCtx middleware")
var channel *Channel
var err error
if channelID := chi.URLParam(r, "channelID"); channelID != "" {
slog.Debug("channel: fetching channel", "channelID", channelID)
channel, err = dbGetChannel(channelID)
} else {
slog.Error("channel: channelID not found in URL parameters")
render.Render(w, r, ErrNotFound)
return
}
if err != nil {
slog.Error("channel: failed to fetch channel", "channelID", chi.URLParam(r, "channelID"), "error", err)
render.Render(w, r, ErrNotFound)
return
}
slog.Debug("channel: successfully fetched channel", "channelID", channel.ID)
ctx := context.WithValue(r.Context(), channelKey{}, channel)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func GetChannel(w http.ResponseWriter, r *http.Request) {
slog.Debug("channel: entering GetChannel handler")
channel, ok := r.Context().Value(channelKey{}).(*Channel)
if !ok || channel == nil {
slog.Error("channel: channel not found in context")
render.Render(w, r, ErrNotFound)
return
}
slog.Debug("channel: rendering channel", "channelID", channel.ID)
if err := render.Render(w, r, NewChannelResponse(channel)); err != nil {
slog.Error("channel: failed to render channel response", "channelID", channel.ID, "error", err)
render.Render(w, r, ErrRender(err))
return
}
}
func ListChannels(w http.ResponseWriter, r *http.Request) {
slog.Debug("channel: entering ListChannels handler")
dbChannels, err := dbGetAllChannels()
if err != nil {
slog.Error("channel: failed to fetch channels", "error", err)
render.Render(w, r, ErrRender(err))
return
}
slog.Debug("channel: successfully fetched channels", "count", len(dbChannels))
if err := render.RenderList(w, r, NewChannelListResponse(dbChannels)); err != nil {
slog.Error("channel: failed to render channel list response", "error", err)
render.Render(w, r, ErrRender(err))
return
}
}
func EditChannel(w http.ResponseWriter, r *http.Request) {
slog.Debug("channel: entering EditChannel handler")
channel, ok := r.Context().Value(channelKey{}).(*Channel)
if !ok || channel == nil {
slog.Error("channel: channel not found in context")
render.Render(w, r, ErrNotFound)
return
}
err := r.ParseMultipartForm(64 << 10)
if err != nil {
slog.Error("channel: failed to parse multipart form", "error", err)
http.Error(w, "Unable to parse form", http.StatusBadRequest)
return
}
channelName := r.FormValue("name")
channeltype := r.FormValue("type")
if channelName == "" || channeltype == "" {
slog.Error("channel: channel name or type is empty")
http.Error(w, "Channel name or type cannot be empty", http.StatusBadRequest)
return
}
slog.Debug("channel: updating channel", "channelID", channel.ID)
channel.Name = channelName
channel.Type = channeltype
err = dbUpdateChannel(channel)
if err != nil {
slog.Error("channel: failed to update channel", "channelID", channel.ID, "error", err)
render.Render(w, r, ErrRender(err))
return
}
slog.Debug("channel: successfully updated channel", "channelID", channel.ID)
if err := render.Render(w, r, NewChannelResponse(channel)); err != nil {
slog.Error("channel: failed to render updated channel response", "channelID", channel.ID, "error", err)
render.Render(w, r, ErrRender(err))
return
}
}
func DeleteChannel(w http.ResponseWriter, r *http.Request) {
slog.Debug("channel: entering DeleteChannel handler")
channel, ok := r.Context().Value(channelKey{}).(*Channel)
if !ok || channel == nil {
slog.Error("channel: channel not found in context")
render.Render(w, r, ErrNotFound)
return
}
slog.Debug("channel: deleting channel", "channelID", channel.ID)
err := dbDeleteChannel(channel.ID.String())
if err != nil {
slog.Error("channel: failed to delete channel", "channelID", channel.ID, "error", err)
render.Render(w, r, ErrRender(err))
return
}
slog.Debug("channel: successfully deleted channel", "channelID", channel.ID)
if err := render.Render(w, r, NewChannelResponse(channel)); err != nil {
slog.Error("channel: failed to render deleted channel response", "channelID", channel.ID, "error", err)
render.Render(w, r, ErrRender(err))
return
}
}
func newChannelID() uuid.UUID {
return uuid.New()
}
func NewChannel(w http.ResponseWriter, r *http.Request) {
slog.Debug("channel: entering NewChannel handler")
err := r.ParseMultipartForm(64 << 10)
if err != nil {
slog.Error("channel: failed to parse multipart form", "error", err)
http.Error(w, "Unable to parse form", http.StatusBadRequest)
return
}
channelName := r.FormValue("name")
channeltype := r.FormValue("type")
if channelName == "" || channeltype == "" {
slog.Error("channel: channel name or type is empty")
http.Error(w, "Channel name or type cannot be empty", http.StatusBadRequest)
return
}
channel := Channel{
ID: newChannelID(),
Name: channelName,
Type: channeltype,
Created: time.Now(),
}
slog.Debug("channel: creating new channel", "channelID", channel.ID)
err = dbAddChannel(&channel)
if err != nil {
slog.Error("channel: failed to add new channel", "channelID", channel.ID, "error", err)
render.Render(w, r, ErrRender(err))
return
}
slog.Debug("channel: successfulyl created new channel", "channelID", channel.ID)
render.Render(w, r, NewChannelResponse(&channel))
}
type channelKey struct{}
type Channel struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Created time.Time `json:"created"`
}
type ChannelRequest struct {
*Channel
}
type ChannelResponse struct {
*Channel
}
func (c ChannelResponse) MarshalJson() ([]byte, error) {
type OrderedChannelResponse struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Created string `json:"created"`
}
ordered := OrderedChannelResponse{
ID: c.Channel.ID,
Name: c.Channel.Name,
Type: c.Channel.Type,
Created: c.Channel.Created.Format(time.RFC3339),
}
return json.Marshal(ordered)
}

View File

@@ -239,3 +239,102 @@ func dbDeleteMessage(id string) error {
slog.Debug("db: message deleted", "messageid", id)
return nil
}
func dbAddChannel(channel *Channel) error {
query := `INSERT INTO channels (id, name, type, created) VALUES (?, ?, ?, ?)`
err := db.Session.Query(query,
channel.ID,
channel.Name,
channel.Type,
channel.Created).Exec()
if err != nil {
slog.Error("db: failed to add channel", "error", err, "channelid", channel.ID)
return fmt.Errorf("failed to add channel")
}
slog.Debug("db: channel added", "channelid", channel.ID)
return nil
}
func dbGetChannel(id string) (*Channel, error) {
query := `SELECT id, name, type, created FROM channels WHERE id = ?`
var channel Channel
err := db.Session.Query(query, id).Scan(
&channel.ID,
&channel.Name,
&channel.Type,
&channel.Created)
if err == gocql.ErrNotFound {
slog.Debug("db: channel not found", "channelid", id)
return nil, errors.New("Channel not found")
} else if err != nil {
slog.Error("db: failed to query channel", "error", err)
return nil, fmt.Errorf("failed to query channel")
}
slog.Debug("db: channel found", "channelid", channel.ID)
return &channel, nil
}
func dbGetAllChannels() ([]*Channel, error) {
query := `SELECT id, name, type, created FROM channels`
iter := db.Session.Query(query).Iter()
defer iter.Close()
var channels []*Channel
for {
channel := &Channel{}
if !iter.Scan(
&channel.ID,
&channel.Name,
&channel.Type,
&channel.Created) {
break
}
channels = append(channels, channel)
}
if err := iter.Close(); err != nil {
slog.Error("db: failed to iterate channels", "error", err)
return nil, fmt.Errorf("failed to iterate channels")
}
if len(channels) == 0 {
slog.Debug("db: no channels found")
return nil, errors.New("no channels found")
}
slog.Debug("db: channel list returned")
return channels, nil
}
func dbUpdateChannel(updatedChannel *Channel) error {
query := `UPDATE channels SET name = ?, type = ?, created = ? WHERE id = ?`
err := db.Session.Query(query,
updatedChannel.Name,
updatedChannel.Type,
updatedChannel.Created,
updatedChannel.ID).Exec()
if err != nil {
slog.Error("db: failed to update channel", "error", err, "channelid", updatedChannel.ID)
return fmt.Errorf("failed to update channel")
}
slog.Debug("db: channel updated", "channelid", updatedChannel.ID)
return nil
}
func dbDeleteChannel(id string) error {
query := `DELETE FROM channels WHERE id = ?`
err := db.Session.Query(query, id).Exec()
if err != nil {
slog.Error("db: failed to delete channel", "error", err)
return fmt.Errorf("failed to delete channel")
}
slog.Debug("db: channel deleted")
return nil
}

View File

@@ -41,3 +41,21 @@ func NewUserListResponse(users []*User) []render.Renderer {
func NewUserPayloadResponse(user *User) *UserPayload {
return &UserPayload{User: user}
}
func NewChannelResponse(channel *Channel) *ChannelResponse {
resp := &ChannelResponse{Channel: channel}
return resp
}
func (cr *ChannelResponse) Render(w http.ResponseWriter, r *http.Request) error {
return nil
}
func NewChannelListResponse(channels []*Channel) []render.Renderer {
list := []render.Renderer{}
for _, channel := range channels {
list = append(list, NewChannelResponse(channel))
}
return list
}

2
go.mod
View File

@@ -13,6 +13,8 @@ require (
github.com/joho/godotenv v1.5.1
)
require github.com/go-chi/cors v1.2.1
require (
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect

2
go.sum
View File

@@ -10,6 +10,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0=
github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-chi/docgen v1.3.0 h1:dmDJ2I+EJfCTrxfgxQDwfR/OpZLTRFKe7EKB8v7yuxI=
github.com/go-chi/docgen v1.3.0/go.mod h1:G9W0G551cs2BFMSn/cnGwX+JBHEloAgo17MBhyrnhPI=
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=

View File

@@ -23,7 +23,7 @@ func main() {
log.Logger() // initialize logger
requiredEnvVars := []string{"SCYLLA_CLUSTER", "SCYLLA_KEYSPACE"}
requiredEnvVars := []string{"SCYLLA_CLUSTER", "SCYLLA_KEYSPACE", "JWT_SECRET", "ALLOWED_ORIGINS"}
// Initialize dotenv file
err := godotenv.Load()

21
vendor/github.com/go-chi/cors/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
Copyright (c) 2014 Olivier Poitrey <rs@dailymotion.com>
Copyright (c) 2016-Present https://github.com/go-chi authors
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

39
vendor/github.com/go-chi/cors/README.md generated vendored Normal file
View File

@@ -0,0 +1,39 @@
# CORS net/http middleware
[go-chi/cors](https://github.com/go-chi/cors) is a fork of [github.com/rs/cors](https://github.com/rs/cors) that
provides a `net/http` compatible middleware for performing preflight CORS checks on the server side. These headers
are required for using the browser native [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API).
This middleware is designed to be used as a top-level middleware on the [chi](https://github.com/go-chi/chi) router.
Applying with within a `r.Group()` or using `With()` will not work without routes matching `OPTIONS` added.
## Usage
```go
func main() {
r := chi.NewRouter()
// Basic CORS
// for more ideas, see: https://developer.github.com/v3/#cross-origin-resource-sharing
r.Use(cors.Handler(cors.Options{
// AllowedOrigins: []string{"https://foo.com"}, // Use this to allow specific origin hosts
AllowedOrigins: []string{"https://*", "http://*"},
// AllowOriginFunc: func(r *http.Request, origin string) bool { return true },
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
ExposedHeaders: []string{"Link"},
AllowCredentials: false,
MaxAge: 300, // Maximum value not ignored by any of major browsers
}))
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("welcome"))
})
http.ListenAndServe(":3000", r)
}
```
## Credits
All credit for the original work of this middleware goes out to [github.com/rs](github.com/rs).

400
vendor/github.com/go-chi/cors/cors.go generated vendored Normal file
View File

@@ -0,0 +1,400 @@
// cors package is net/http handler to handle CORS related requests
// as defined by http://www.w3.org/TR/cors/
//
// You can configure it by passing an option struct to cors.New:
//
// c := cors.New(cors.Options{
// AllowedOrigins: []string{"foo.com"},
// AllowedMethods: []string{"GET", "POST", "DELETE"},
// AllowCredentials: true,
// })
//
// Then insert the handler in the chain:
//
// handler = c.Handler(handler)
//
// See Options documentation for more options.
//
// The resulting handler is a standard net/http handler.
package cors
import (
"log"
"net/http"
"os"
"strconv"
"strings"
)
// Options is a configuration container to setup the CORS middleware.
type Options struct {
// AllowedOrigins is a list of origins a cross-domain request can be executed from.
// If the special "*" value is present in the list, all origins will be allowed.
// An origin may contain a wildcard (*) to replace 0 or more characters
// (i.e.: http://*.domain.com). Usage of wildcards implies a small performance penalty.
// Only one wildcard can be used per origin.
// Default value is ["*"]
AllowedOrigins []string
// AllowOriginFunc is a custom function to validate the origin. It takes the origin
// as argument and returns true if allowed or false otherwise. If this option is
// set, the content of AllowedOrigins is ignored.
AllowOriginFunc func(r *http.Request, origin string) bool
// AllowedMethods is a list of methods the client is allowed to use with
// cross-domain requests. Default value is simple methods (HEAD, GET and POST).
AllowedMethods []string
// AllowedHeaders is list of non simple headers the client is allowed to use with
// cross-domain requests.
// If the special "*" value is present in the list, all headers will be allowed.
// Default value is [] but "Origin" is always appended to the list.
AllowedHeaders []string
// ExposedHeaders indicates which headers are safe to expose to the API of a CORS
// API specification
ExposedHeaders []string
// AllowCredentials indicates whether the request can include user credentials like
// cookies, HTTP authentication or client side SSL certificates.
AllowCredentials bool
// MaxAge indicates how long (in seconds) the results of a preflight request
// can be cached
MaxAge int
// OptionsPassthrough instructs preflight to let other potential next handlers to
// process the OPTIONS method. Turn this on if your application handles OPTIONS.
OptionsPassthrough bool
// Debugging flag adds additional output to debug server side CORS issues
Debug bool
}
// Logger generic interface for logger
type Logger interface {
Printf(string, ...interface{})
}
// Cors http handler
type Cors struct {
// Debug logger
Log Logger
// Normalized list of plain allowed origins
allowedOrigins []string
// List of allowed origins containing wildcards
allowedWOrigins []wildcard
// Optional origin validator function
allowOriginFunc func(r *http.Request, origin string) bool
// Normalized list of allowed headers
allowedHeaders []string
// Normalized list of allowed methods
allowedMethods []string
// Normalized list of exposed headers
exposedHeaders []string
maxAge int
// Set to true when allowed origins contains a "*"
allowedOriginsAll bool
// Set to true when allowed headers contains a "*"
allowedHeadersAll bool
allowCredentials bool
optionPassthrough bool
}
// New creates a new Cors handler with the provided options.
func New(options Options) *Cors {
c := &Cors{
exposedHeaders: convert(options.ExposedHeaders, http.CanonicalHeaderKey),
allowOriginFunc: options.AllowOriginFunc,
allowCredentials: options.AllowCredentials,
maxAge: options.MaxAge,
optionPassthrough: options.OptionsPassthrough,
}
if options.Debug && c.Log == nil {
c.Log = log.New(os.Stdout, "[cors] ", log.LstdFlags)
}
// Normalize options
// Note: for origins and methods matching, the spec requires a case-sensitive matching.
// As it may error prone, we chose to ignore the spec here.
// Allowed Origins
if len(options.AllowedOrigins) == 0 {
if options.AllowOriginFunc == nil {
// Default is all origins
c.allowedOriginsAll = true
}
} else {
c.allowedOrigins = []string{}
c.allowedWOrigins = []wildcard{}
for _, origin := range options.AllowedOrigins {
// Normalize
origin = strings.ToLower(origin)
if origin == "*" {
// If "*" is present in the list, turn the whole list into a match all
c.allowedOriginsAll = true
c.allowedOrigins = nil
c.allowedWOrigins = nil
break
} else if i := strings.IndexByte(origin, '*'); i >= 0 {
// Split the origin in two: start and end string without the *
w := wildcard{origin[0:i], origin[i+1:]}
c.allowedWOrigins = append(c.allowedWOrigins, w)
} else {
c.allowedOrigins = append(c.allowedOrigins, origin)
}
}
}
// Allowed Headers
if len(options.AllowedHeaders) == 0 {
// Use sensible defaults
c.allowedHeaders = []string{"Origin", "Accept", "Content-Type"}
} else {
// Origin is always appended as some browsers will always request for this header at preflight
c.allowedHeaders = convert(append(options.AllowedHeaders, "Origin"), http.CanonicalHeaderKey)
for _, h := range options.AllowedHeaders {
if h == "*" {
c.allowedHeadersAll = true
c.allowedHeaders = nil
break
}
}
}
// Allowed Methods
if len(options.AllowedMethods) == 0 {
// Default is spec's "simple" methods
c.allowedMethods = []string{http.MethodGet, http.MethodPost, http.MethodHead}
} else {
c.allowedMethods = convert(options.AllowedMethods, strings.ToUpper)
}
return c
}
// Handler creates a new Cors handler with passed options.
func Handler(options Options) func(next http.Handler) http.Handler {
c := New(options)
return c.Handler
}
// AllowAll create a new Cors handler with permissive configuration allowing all
// origins with all standard methods with any header and credentials.
func AllowAll() *Cors {
return New(Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{
http.MethodHead,
http.MethodGet,
http.MethodPost,
http.MethodPut,
http.MethodPatch,
http.MethodDelete,
},
AllowedHeaders: []string{"*"},
AllowCredentials: false,
})
}
// Handler apply the CORS specification on the request, and add relevant CORS headers
// as necessary.
func (c *Cors) Handler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodOptions && r.Header.Get("Access-Control-Request-Method") != "" {
c.logf("Handler: Preflight request")
c.handlePreflight(w, r)
// Preflight requests are standalone and should stop the chain as some other
// middleware may not handle OPTIONS requests correctly. One typical example
// is authentication middleware ; OPTIONS requests won't carry authentication
// headers (see #1)
if c.optionPassthrough {
next.ServeHTTP(w, r)
} else {
w.WriteHeader(http.StatusOK)
}
} else {
c.logf("Handler: Actual request")
c.handleActualRequest(w, r)
next.ServeHTTP(w, r)
}
})
}
// handlePreflight handles pre-flight CORS requests
func (c *Cors) handlePreflight(w http.ResponseWriter, r *http.Request) {
headers := w.Header()
origin := r.Header.Get("Origin")
if r.Method != http.MethodOptions {
c.logf("Preflight aborted: %s!=OPTIONS", r.Method)
return
}
// Always set Vary headers
// see https://github.com/rs/cors/issues/10,
// https://github.com/rs/cors/commit/dbdca4d95feaa7511a46e6f1efb3b3aa505bc43f#commitcomment-12352001
headers.Add("Vary", "Origin")
headers.Add("Vary", "Access-Control-Request-Method")
headers.Add("Vary", "Access-Control-Request-Headers")
if origin == "" {
c.logf("Preflight aborted: empty origin")
return
}
if !c.isOriginAllowed(r, origin) {
c.logf("Preflight aborted: origin '%s' not allowed", origin)
return
}
reqMethod := r.Header.Get("Access-Control-Request-Method")
if !c.isMethodAllowed(reqMethod) {
c.logf("Preflight aborted: method '%s' not allowed", reqMethod)
return
}
reqHeaders := parseHeaderList(r.Header.Get("Access-Control-Request-Headers"))
if !c.areHeadersAllowed(reqHeaders) {
c.logf("Preflight aborted: headers '%v' not allowed", reqHeaders)
return
}
if c.allowedOriginsAll {
headers.Set("Access-Control-Allow-Origin", "*")
} else {
headers.Set("Access-Control-Allow-Origin", origin)
}
// Spec says: Since the list of methods can be unbounded, simply returning the method indicated
// by Access-Control-Request-Method (if supported) can be enough
headers.Set("Access-Control-Allow-Methods", strings.ToUpper(reqMethod))
if len(reqHeaders) > 0 {
// Spec says: Since the list of headers can be unbounded, simply returning supported headers
// from Access-Control-Request-Headers can be enough
headers.Set("Access-Control-Allow-Headers", strings.Join(reqHeaders, ", "))
}
if c.allowCredentials {
headers.Set("Access-Control-Allow-Credentials", "true")
}
if c.maxAge > 0 {
headers.Set("Access-Control-Max-Age", strconv.Itoa(c.maxAge))
}
c.logf("Preflight response headers: %v", headers)
}
// handleActualRequest handles simple cross-origin requests, actual request or redirects
func (c *Cors) handleActualRequest(w http.ResponseWriter, r *http.Request) {
headers := w.Header()
origin := r.Header.Get("Origin")
// Always set Vary, see https://github.com/rs/cors/issues/10
headers.Add("Vary", "Origin")
if origin == "" {
c.logf("Actual request no headers added: missing origin")
return
}
if !c.isOriginAllowed(r, origin) {
c.logf("Actual request no headers added: origin '%s' not allowed", origin)
return
}
// Note that spec does define a way to specifically disallow a simple method like GET or
// POST. Access-Control-Allow-Methods is only used for pre-flight requests and the
// spec doesn't instruct to check the allowed methods for simple cross-origin requests.
// We think it's a nice feature to be able to have control on those methods though.
if !c.isMethodAllowed(r.Method) {
c.logf("Actual request no headers added: method '%s' not allowed", r.Method)
return
}
if c.allowedOriginsAll {
headers.Set("Access-Control-Allow-Origin", "*")
} else {
headers.Set("Access-Control-Allow-Origin", origin)
}
if len(c.exposedHeaders) > 0 {
headers.Set("Access-Control-Expose-Headers", strings.Join(c.exposedHeaders, ", "))
}
if c.allowCredentials {
headers.Set("Access-Control-Allow-Credentials", "true")
}
c.logf("Actual response added headers: %v", headers)
}
// convenience method. checks if a logger is set.
func (c *Cors) logf(format string, a ...interface{}) {
if c.Log != nil {
c.Log.Printf(format, a...)
}
}
// isOriginAllowed checks if a given origin is allowed to perform cross-domain requests
// on the endpoint
func (c *Cors) isOriginAllowed(r *http.Request, origin string) bool {
if c.allowOriginFunc != nil {
return c.allowOriginFunc(r, origin)
}
if c.allowedOriginsAll {
return true
}
origin = strings.ToLower(origin)
for _, o := range c.allowedOrigins {
if o == origin {
return true
}
}
for _, w := range c.allowedWOrigins {
if w.match(origin) {
return true
}
}
return false
}
// isMethodAllowed checks if a given method can be used as part of a cross-domain request
// on the endpoint
func (c *Cors) isMethodAllowed(method string) bool {
if len(c.allowedMethods) == 0 {
// If no method allowed, always return false, even for preflight request
return false
}
method = strings.ToUpper(method)
if method == http.MethodOptions {
// Always allow preflight requests
return true
}
for _, m := range c.allowedMethods {
if m == method {
return true
}
}
return false
}
// areHeadersAllowed checks if a given list of headers are allowed to used within
// a cross-domain request.
func (c *Cors) areHeadersAllowed(requestedHeaders []string) bool {
if c.allowedHeadersAll || len(requestedHeaders) == 0 {
return true
}
for _, header := range requestedHeaders {
header = http.CanonicalHeaderKey(header)
found := false
for _, h := range c.allowedHeaders {
if h == header {
found = true
break
}
}
if !found {
return false
}
}
return true
}

70
vendor/github.com/go-chi/cors/utils.go generated vendored Normal file
View File

@@ -0,0 +1,70 @@
package cors
import "strings"
const toLower = 'a' - 'A'
type converter func(string) string
type wildcard struct {
prefix string
suffix string
}
func (w wildcard) match(s string) bool {
return len(s) >= len(w.prefix+w.suffix) && strings.HasPrefix(s, w.prefix) && strings.HasSuffix(s, w.suffix)
}
// convert converts a list of string using the passed converter function
func convert(s []string, c converter) []string {
out := []string{}
for _, i := range s {
out = append(out, c(i))
}
return out
}
// parseHeaderList tokenize + normalize a string containing a list of headers
func parseHeaderList(headerList string) []string {
l := len(headerList)
h := make([]byte, 0, l)
upper := true
// Estimate the number headers in order to allocate the right splice size
t := 0
for i := 0; i < l; i++ {
if headerList[i] == ',' {
t++
}
}
headers := make([]string, 0, t)
for i := 0; i < l; i++ {
b := headerList[i]
if b >= 'a' && b <= 'z' {
if upper {
h = append(h, b-toLower)
} else {
h = append(h, b)
}
} else if b >= 'A' && b <= 'Z' {
if !upper {
h = append(h, b+toLower)
} else {
h = append(h, b)
}
} else if b == '-' || b == '_' || b == '.' || (b >= '0' && b <= '9') {
h = append(h, b)
}
if b == ' ' || b == ',' || i == l-1 {
if len(h) > 0 {
// Flush the found header
headers = append(headers, string(h))
h = h[:0]
upper = true
}
} else {
upper = b == '-'
}
}
return headers
}

3
vendor/modules.txt vendored
View File

@@ -5,6 +5,9 @@ github.com/ajg/form
## explicit; go 1.14
github.com/go-chi/chi/v5
github.com/go-chi/chi/v5/middleware
# github.com/go-chi/cors v1.2.1
## explicit; go 1.14
github.com/go-chi/cors
# github.com/go-chi/docgen v1.3.0
## explicit; go 1.15
github.com/go-chi/docgen