fcors

package module
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 18, 2023 License: MIT Imports: 2 Imported by: 1

README

jub0bs/fcors

godoc license build codecov goreport

A principled CORS middleware library for Go.

About CORS

The Same-Origin Policy (SOP) is a security mechanism that Web browsers implement to protect their users. In particular, the SOP restricts cross-origin network access in terms of both sending and reading. Cross-Origin Resource Sharing (CORS) is a protocol that lets servers instruct browsers to relax those restrictions for select clients.

jub0bs/fcors allows you to configure and build net/http middleware that implement CORS.

Design philosophy

jub0bs/fcors is designed to be both easier to use and harder to misuse than other CORS middleware libraries; see Fearless CORS: a design philosophy for CORS middleware libraries (and a Go implementation) and Useful Functional-Options Tricks for Better Libraries (GopherCon Europe 2023).

Praise for jub0bs/fcors

I really like the declarative API. It lets you say what behavior you want rather than setting specific headers. It means that, as a user, you don’t have to relearn the nuances of CORS every time you want to make a change.

Paul Carleton (Staff Software Engineer at Stripe)

Installation

go get github.com/jub0bs/fcors

jub0bs/fcors requires Go 1.21 or above.

Example

The following program builds a CORS middleware that allows anonymous requests from Web origin https://example.com, with any HTTP method among GET, POST, PUT, or DELETE, and possibly with request header Authorization. The CORS middleware in question is then applied to a simple handler bound to the /hello endpoint.

package main

import (
  "fmt"
  "io"
  "net/http"
  "os"

  "github.com/jub0bs/fcors"
)

func main() {
  cors, err := fcors.AllowAccess(
    fcors.FromOrigins("https://example.com"),
    fcors.WithMethods(
      http.MethodGet,
      http.MethodPost,
      http.MethodPut,
      http.MethodDelete,
    ),
    fcors.WithRequestHeaders("Authorization"),
  )
  if err != nil {
    fmt.Fprintln(os.Stderr, err)
    os.Exit(1)
  }
  http.Handle("/hello", cors(http.HandlerFunc(helloHandler)))
  if err := http.ListenAndServe(":8080", nil); err != nil {
    fmt.Fprintln(os.Stderr, err)
    os.Exit(1)
  }
}

func helloHandler(w http.ResponseWriter, _ *http.Request) {
  io.WriteString(w, "Hello, world!\n")
}

Try it out yourself by saving this program to a file named server.go. You may need to adjust the port number if port 8080 happens to be unavailable on your machine. Then build and run your server:

go build server.go
./server

If no error occurred, the server is now running on localhost:8080 and the /hello resource is now configured for CORS as desired.

Documentation

The documentation is available on pkg.go.dev.

Moreover, guidance on how to use jub0bs/fcors with third-party Web frameworks and HTTP toolkits can be found in jub0bs/fcors-examples.

Code coverage

coverage

Benchmarks

Some benchmarks pitting jub0bs/fcors against rs/cors are available in jub0bs/fcors-benchmarks.

License

All source code is covered by the MIT License.

Documentation

Overview

Package fcors provides net/http middleware for Cross-Origin Resource Sharing (CORS).

To create a CORS middleware that only allows anonymous access, use the AllowAccess function. To create a CORS middleware that allows both anonymous access and credentialed access (e.g. with cookies), use the AllowAccessWithCredentials function.

Note that, for things to work properly, fcors users must follow certain rules; the key words "MUST", "MUST NOT", "SHOULD", "SHOULD NOT", and "MAY" below are to be interpreted as described in RFC 2119:

  • Because CORS-preflight requests use OPTIONS as their method, the resources to which you apply a CORS middleware SHOULD accept OPTIONS requests.
  • Because CORS-preflight requests are not authenticated, a CORS middleware SHOULD be stacked on top of any authentication middleware.
  • Multiple CORS middleware MUST NOT be stacked; in other words, no more than one CORS middleware MUST be used per resource.
  • Other middleware (if any) in the chain MUST NOT alter any CORS response headers that are set by this library's middleware and MUST NOT add more CORS response headers.
  • Other middleware (if any) in the chain SHOULD NOT alter any Vary header that is set by this library's middleware, but they MAY add more Vary headers.

This package provides basic options for configuring a CORS middleware, but more advanced (and potentially dangerous) options can be found in the github.com/jub0bs/fcors/risky package.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Middleware

type Middleware = func(http.Handler) http.Handler

Middleware is a convenience alias for the type of a function that takes a http.Handler and returns a http.Handler.

The middleware provided by this package are, of course, safe for concurrent use by multiple goroutines.

func AllowAccess

func AllowAccess(one OptionAnon, others ...OptionAnon) (Middleware, error)

AllowAccess creates a CORS middleware that only allows anonymous access, according to the specified options. The behavior of the resulting middleware is insensitive to the order in which the options that configure it are specified.

AllowAccess requires a single call to option FromOrigins or option FromAnyOrigin as one of its arguments.

Using a given option more than once in a call to AllowAccess results in a failure to build the corresponding middleware.

If the specified options are invalid or mutually incompatible, AllowAccess returns a nil Middleware and some non-nil error. Otherwise, it returns a functioning Middleware and a nil error.

Any occurrence of a nil option results in a panic.

Example
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"

	"github.com/jub0bs/fcors"
)

func main() {
	cors, err := fcors.AllowAccess(
		fcors.FromAnyOrigin(),
		fcors.WithMethods(
			http.MethodGet,
			http.MethodDelete,
			http.MethodPost,
			http.MethodPut,
		),
		fcors.WithRequestHeaders(
			"Authorization",
			"Content-Type",
		),
		fcors.MaxAgeInSeconds(30),
	)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	helloHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		io.WriteString(w, "Hello, world!\n")
	})
	http.Handle("/hello", cors(helloHandler))
	if err := http.ListenAndServe(":8080", nil); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}
Output:

func AllowAccessWithCredentials

func AllowAccessWithCredentials(one Option, others ...Option) (Middleware, error)

AllowAccessWithCredentials creates a CORS middleware that allows both anonymous access and credentialed access (e.g. with cookies), according to the specified options. The behavior of the resulting middleware is insensitive to the order in which the options that configure it are specified.

AllowAccessWithCredentials requires a single call to option FromOrigins as one of its arguments.

Using a given option more than once in a call to AllowAccessWithCredentials results in a failure to build the corresponding middleware.

If the specified options are invalid or mutually incompatible, AllowAccessWithCredentials returns a nil Middleware and some non-nil error. Otherwise, it returns a functioning Middleware and a nil error.

Any occurrence of a nil option results in a panic.

Example
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"

	"github.com/jub0bs/fcors"
)

func main() {
	cors, err := fcors.AllowAccessWithCredentials(
		fcors.FromOrigins(
			"https://example.com",
		),
		fcors.WithMethods(
			http.MethodGet,
			http.MethodDelete,
			http.MethodPost,
			http.MethodPut,
		),
		fcors.WithRequestHeaders(
			"Content-Type",
		),
		fcors.MaxAgeInSeconds(30),
	)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	helloHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		io.WriteString(w, "Hello, world!\n")
	})
	http.Handle("/hello", cors(helloHandler))
	if err := http.ListenAndServe(":8080", nil); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}
Output:

type Option

type Option = internal.Option

An Option configures a CORS middleware that allows both anonymous access and credentialed access (e.g. with cookies).

You're not meant to implement this interface.

func ExposeResponseHeaders

func ExposeResponseHeaders(one string, others ...string) Option

ExposeResponseHeaders configures a CORS middleware to expose all of the specified response headers to the client.

Using this option in conjunction with option ExposeAllResponseHeaders in a call to AllowAccess results in a failure to build the corresponding middleware.

Any occurrence of an invalid header name results in a failure to build the corresponding middleware.

Header names are case-insensitive. Specifying the same header name multiple times (possibly using different cases) results in a failure to build the corresponding middleware.

The CORS protocol defines a number of so-called "CORS-safelisted response-header names", which are always accessible to the client. The CORS protocol also defines a number of so-called "forbidden response-header names", which are never accessible to the client. Specifying one or more safelisted or forbidden response-header name(s) results in a failure to build the corresponding middleware.

Finally, some header names that have no place in a response are prohibited:

  • Access-Control-Request-Headers
  • Access-Control-Request-Method
  • Access-Control-Request-Private-Network
  • Origin

Although a valid response-header name, a literal * is also prohibited; to expose all response headers, use option ExposeAllResponseHeaders instead of this one.

func FromOrigins

func FromOrigins(one string, others ...string) Option

FromOrigins configures a CORS middleware to allow access from any of the Web origins encompassed by the specified origin patterns.

Using this option in conjunction with option FromAnyOrigin in a call to AllowAccess results in a failure to build the corresponding middleware. Any occurrence of a prohibited pattern results in a failure to build the corresponding middleware.

Permitted schemes are limited to http (with a caveat explained further down) and https:

http://example.com             // permitted
https://example.com            // permitted
chrome-extension://example.com // prohibited

Origins must be specified in ASCII serialized form; Unicode is prohibited:

https://example.com            // permitted
https://www.xn--xample-9ua.com // permitted (Punycode)
https://www.éxample.com        // prohibited (Unicode)

For security reasons, the null origin is prohibited.

Hosts that are IPv4 addresses must be specified in dotted-quad notation:

http://255.0.0.0  // permitted
http://0xFF000000 // prohibited

Hosts that are IPv6 addresses must be specified in their compressed form:

http://[::1]:9090                                     // permitted
http://[0:0:0:0:0:0:0:0001]:9090                      // prohibited
http://[0000:0000:0000:0000:0000:0000:0000:0001]:9090 // prohibited

Default ports (80 for http, 443 for https) must be elided:

http://example.com      // permitted
http://example.com:80   // prohibited
https://example.com     // permitted
https://example.com:443 // prohibited

In addition to support for exact origins, this option provides limited support for origin patterns that encompass multiple origins. A leading asterisk (followed by a full stop) in a host pattern denotes exactly one arbitrary DNS label. For instance,

https://*.example.com

encompasses the following origins (among others),

https://foo.example.com
https://bar.example.com

but not the following origin,

https://bar.foo.example.com

Two leading asterisks (followed by a full stop) in a host pattern denote one or more arbitrary DNS label(s). For instance,

https://**.example.com

encompasses the following origins (among others),

https://foo.example.com
https://bar.example.com
https://bar.foo.example.com
https://baz.bar.foo.example.com

An asterisk in place of a port denotes an arbitrary (possibly implicit) port. For instance,

http://localhost:*

encompasses the following origins (among others),

http://localhost
http://localhost:80
http://localhost:9090

Specifying both arbitrary subdomains and arbitrary ports in a given origin pattern is prohibited:

https://*.example.com:*     // prohibited
https://**.example.com:*    // prohibited
https://*.example.com       // permitted
https://**.example.com      // permitted
https://*.example.com:9090  // permitted
https://**.example.com:9090 // permitted
https://example.com:*       // permitted

No other types of origin patterns are supported. In particular, a single asterisk is a prohibited origin pattern. If you want to allow (anonymous) access from all origins, use option FromAnyOrigin instead of this one.

Origins whose scheme is http and whose host is neither localhost nor a loopback IP address are deemed insecure; as such, they are by default prohibited. If you need to deliberately allow insecure origins (danger!), you must also activate option github.com/jub0bs/fcors/risky.TolerateInsecureOrigins. Any occurrence of an insecure origin without activating option github.com/jub0bs/fcors/risky.TolerateInsecureOrigins results in a failure to build the corresponding middleware.

Also for security reasons, allowing arbitrary subdomains of a base domain that happens to be a public suffix is by default prohibited:

https://*.com          // prohibited (by default): com is a public suffix
https://**.com         // prohibited (by default): com is a public suffix
https://*.github.io    // prohibited (by default): github.io is a public suffix
https://**.github.io   // prohibited (by default): github.io is a public suffix
https://*.example.com  // ok
https://**.example.com // ok

If you need to deliberately allow arbitrary subdomains of a public suffix (danger!), you must also activate option github.com/jub0bs/fcors/risky.SkipPublicSuffixCheck. Any occurrence of such a prohibited origin pattern without activating option github.com/jub0bs/fcors/risky.SkipPublicSuffixCheck results in a failure to build the corresponding middleware.

func MaxAgeInSeconds

func MaxAgeInSeconds(delta uint) Option

MaxAgeInSeconds configures a CORS middleware to intruct browsers to cache preflight responses for a maximum duration of delta seconds.

Specifying a max-age value of 0 instructs browsers to eschew caching of preflight responses altogether, whereas omitting to specify a max age causes browsers to cache preflight responses with a default max-age value of 5 seconds.

Because all modern browsers cap the max-age value (the larger upper bound currently is Firefox's: 86,400 seconds), this option accordingly imposes an upper bound on its argument: attempts to specify a max-age value larger than 86400 result in a failure to build the corresponding middleware.

func PreflightSuccessStatus

func PreflightSuccessStatus(code uint) Option

PreflightSuccessStatus configures a CORS middleware to use the specified status code in successful preflight responses.

When this option is not used, the status used in successful preflight responses is 204 No Content.

Specifying a custom status code outside the 2xx range results in a failure to build the corresponding middleware.

func WithAnyMethod

func WithAnyMethod() Option

WithAnyMethod configures a CORS middleware to allow any HTTP method.

Using this option in conjunction with option WithMethods in a call to AllowAccess or AllowAccessWithCredentials results in a failure to build the corresponding middleware.

func WithAnyRequestHeaders

func WithAnyRequestHeaders() Option

WithAnyRequestHeaders configures a CORS middleware to allow any request headers.

Using this option in conjunction with option WithRequestHeaders in a call to AllowAccess or AllowAccessWithCredentials results in a failure to build the corresponding middleware.

func WithMethods

func WithMethods(one string, others ...string) Option

WithMethods configures a CORS middleware to allow any of the specified HTTP methods.

Using this option in conjunction with option WithAnyMethod in a call to AllowAccess or AllowAccessWithCredentials results in a failure to build the corresponding middleware.

Method names are case-sensitive.

The three so-called "CORS-safelisted methods"" (GET, HEAD, and POST) are by default allowed by the CORS protocol. As such, allowing them explicitly in your CORS configuration is harmless but never actually necessary.

Moreover, the CORS protocol forbids the use of some method names. Accordingly, any occurrence of an invalid or forbidden method name results in a failure to build the corresponding middleware.

Although a valid method name, a literal * is also prohibited; to allow all methods, use option WithAnyMethod instead of this one.

Note that, contrary to popular belief, configuring a CORS middleware to allow the OPTIONS method is only required if some clients actually make explicit use of that method, e.g.

fetch('https://example.com', {method: 'OPTIONS'})

In the great majority of cases, allowing the OPTIONS method is unnecessary.

func WithRequestHeaders

func WithRequestHeaders(one string, others ...string) Option

WithRequestHeaders configures a CORS middleware to allow all of the specified request headers to the client.

Using this option in conjunction with option WithAnyRequestHeaders in a call to AllowAccess or AllowAccessWithCredentials results in a failure to build the corresponding middleware.

Any occurrence of an invalid header name results in a failure to build the corresponding middleware.

Header names are case-insensitive. Specifying the same header name multiple times (possibly using different cases) results in a failure to build the corresponding middleware.

The CORS protocol defines a number of so-called "forbidden request-header names", which are never allowed and that browsers silently drop from client requests. Specifying one or more forbidden request-header name(s) results in a failure to build the corresponding middleware.

Finally, some header names that have no place in a request are prohibited:

  • Access-Control-Allow-Credentials
  • Access-Control-Allow-Headers
  • Access-Control-Allow-Methods
  • Access-Control-Allow-Origin
  • Access-Control-Allow-Private-Network
  • Access-Control-Expose-Headers
  • Access-Control-Max-Age

Although a valid request-header name, a literal * is also prohibited; to allow all request headers, use option WithAnyRequestHeaders instead of this one.

type OptionAnon

type OptionAnon = internal.OptionAnon

An OptionAnon configures a CORS middleware that only allows anonymous access.

You're not meant to implement this interface.

func ExposeAllResponseHeaders

func ExposeAllResponseHeaders() OptionAnon

ExposeAllResponseHeaders configures a CORS middleware to expose all response headers.

Using this option in conjunction with option ExposeResponseHeaders in a call to AllowAccess results in a failure to build the corresponding middleware.

func FromAnyOrigin

func FromAnyOrigin() OptionAnon

FromAnyOrigin configures a CORS middleware to allow any Web origin.

Using this option in conjunction with option FromOrigins in a call to AllowAccess results in a failure to build the corresponding middleware.

Directories

Path Synopsis
Package internal hides all implementation details of packages github.com/jub0bs/fcors and github.com/jub0bs/fcors/risky.
Package internal hides all implementation details of packages github.com/jub0bs/fcors and github.com/jub0bs/fcors/risky.
origin
Package origin implements parsing of origins and origin specs and provides data structures useful for representing a set of origins.
Package origin implements parsing of origins and origin specs and provides data structures useful for representing a set of origins.
util
Package util provides a generic type representing a mathematical set, and convenience functions for generating error values for packages github.com/jub0bs/fcors and github.com/jub0bs/fcors/risky.
Package util provides a generic type representing a mathematical set, and convenience functions for generating error values for packages github.com/jub0bs/fcors and github.com/jub0bs/fcors/risky.
Package risky provides additional options that complement those provided by package github.com/jub0bs/fcors but that are potentially dangerous.
Package risky provides additional options that complement those provided by package github.com/jub0bs/fcors but that are potentially dangerous.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL