feat: add configurable transport-independent CORS policy
This commit is contained in:
121
scripts/corsBrowser/main.go
Normal file
121
scripts/corsBrowser/main.go
Normal file
@@ -0,0 +1,121 @@
|
||||
// Run from the framework root; open the printed URLs and click Run checks.
|
||||
// All listeners are loopback-only and stop after five minutes or SIGINT/SIGTERM.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
omni "git.yuntuo.hk/k2safe/go-omni"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
func run() error {
|
||||
var listeners []net.Listener
|
||||
defer func() {
|
||||
for _, l := range listeners {
|
||||
l.Close()
|
||||
}
|
||||
}()
|
||||
for i := 0; i < 4; i++ {
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
listeners = append(listeners, l)
|
||||
}
|
||||
url := func(i int) string { return "http://" + listeners[i].Addr().String() }
|
||||
app := omni.New(omni.WithCORS(omni.CORSConfig{Enabled: true, AllowedOrigins: []string{url(0)}, AllowedMethods: []string{"GET", "POST"}, AllowedHeaders: []string{"Authorization", "Content-Type"}, ExposedHeaders: []string{"Content-Disposition"}, AllowCredentials: true}))
|
||||
defer app.Close()
|
||||
app.GET("/text", func(c *omni.Context) error { return c.Text(200, "hello") })
|
||||
app.GET("/private", func(c *omni.Context) error {
|
||||
if c.Header("Authorization") != "Bearer example-only" {
|
||||
return omni.Unauthorized("LOGIN_REQUIRED", "login required")
|
||||
}
|
||||
return c.Text(200, "authenticated")
|
||||
})
|
||||
app.GET("/download", func(c *omni.Context) error {
|
||||
c.SetResponseHeader("Content-Disposition", "attachment; filename=example.txt")
|
||||
return c.Text(200, "file bytes")
|
||||
})
|
||||
public := omni.New(omni.WithCORS(omni.CORSConfig{Enabled: true, AllowedOrigins: []string{"*"}}))
|
||||
defer public.Close()
|
||||
public.GET("/text", func(c *omni.Context) error { return c.Text(200, "public") })
|
||||
page := func(allowed bool) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
data, _ := json.Marshal(map[string]any{"api": url(2), "publicApi": url(3), "allowed": allowed})
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
io.WriteString(w, strings.Replace(pageSource, "CONFIG_PLACEHOLDER", string(data), 1))
|
||||
})
|
||||
}
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
|
||||
defer cancel()
|
||||
errors := make(chan error, 4)
|
||||
var servers []*http.Server
|
||||
for i, h := range []http.Handler{page(true), page(false), app, public} {
|
||||
s := &http.Server{Handler: h, ReadHeaderTimeout: 3 * time.Second, ReadTimeout: 5 * time.Second, WriteTimeout: 5 * time.Second}
|
||||
servers = append(servers, s)
|
||||
go func(l net.Listener) { errors <- s.Serve(l) }(listeners[i])
|
||||
}
|
||||
defer func() {
|
||||
for _, s := range servers {
|
||||
s.Close()
|
||||
}
|
||||
}()
|
||||
fmt.Printf("Allowed page: %s\nDenied page: %s\n", url(0), url(1))
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case err := <-errors:
|
||||
if err != http.ErrServerClosed {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
const pageSource = `<!doctype html><meta charset="utf-8"><title>GoOmni CORS browser checks</title>
|
||||
<h1>GoOmni CORS browser checks</h1><p>Loopback-only, synthetic data; no accounts or credentials required.</p>
|
||||
<button id="run">Run checks</button><pre id="results">Not started</pre>
|
||||
<script>
|
||||
const cfg=CONFIG_PLACEHOLDER;
|
||||
document.querySelector('#run').onclick=async()=>{
|
||||
const results=[];const out=document.querySelector('#results');out.textContent='Running';
|
||||
async function check(name,path,options,expected,blocked=false,header=false){
|
||||
try{const r=await fetch(path,options);const body=await r.text();
|
||||
results.push({name,pass:!blocked&&r.status===expected&&(!header||r.headers.get('Content-Disposition')==='attachment; filename=example.txt'),status:r.status,body});
|
||||
}catch(e){results.push({name,pass:blocked&&e.name==='TypeError',blocked:true,error:e.name});}
|
||||
}
|
||||
await check('simple GET',cfg.api+'/text',{},200,!cfg.allowed);
|
||||
await check('401 readable',cfg.api+'/private',{},401,!cfg.allowed);
|
||||
await check('authorization preflight',cfg.api+'/private',{headers:{Authorization:'Bearer example-only'}},200,!cfg.allowed);
|
||||
await check('unknown header rejected',cfg.api+'/private',{headers:{'X-Not-Allowed':'example'}},0,true);
|
||||
await check('unknown method rejected',cfg.api+'/text',{method:'DELETE'},0,true);
|
||||
await check('404 readable',cfg.api+'/missing',{},404,!cfg.allowed);
|
||||
await check('credentials mode allowed',cfg.api+'/text',{credentials:'include'},200,!cfg.allowed);
|
||||
await check('wildcard without credentials',cfg.publicApi+'/text',{},200);
|
||||
await check('wildcard credentials blocked',cfg.publicApi+'/text',{credentials:'include'},0,true);
|
||||
await check('exposed download header',cfg.api+'/download',{},200,!cfg.allowed,cfg.allowed);
|
||||
out.textContent=JSON.stringify({allowedPage:cfg.allowed,passed:results.every(r=>r.pass),checks:results},null,2);
|
||||
};
|
||||
</script>`
|
||||
Reference in New Issue
Block a user