122 lines
4.7 KiB
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>`
|