Files
go-omni/scripts/corsBrowser/main.go

122 lines
4.7 KiB
Go

// 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>`