Files

184 lines
6.2 KiB
Go

// signedRead is an offline, loopback-only preview/download demonstration.
package main
import (
"bytes"
"context"
"crypto/rand"
"errors"
"flag"
"fmt"
"html/template"
"io"
"mime"
"net"
"net/http"
"os"
"os/signal"
"strconv"
"time"
"git.yuntuo.hk/k2safe/go-omni/storage"
"git.yuntuo.hk/k2safe/go-omni/storage/local"
)
func main() {
check := flag.Bool("check", false, "run HTTP checks and exit")
flag.Parse()
if err := run(*check); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run(check bool) error {
root, err := os.MkdirTemp("", "go-omni-signed-read-")
if err != nil {
return err
}
defer os.RemoveAll(root)
files, err := local.New(root, 1<<20)
if err != nil {
return err
}
content := samplePDF()
if _, err = files.Put(context.Background(), "sample.pdf", bytes.NewReader(content), int64(len(content)), "application/pdf"); err != nil {
return err
}
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return err
}
defer listener.Close()
baseURL := "http://" + listener.Addr().String()
secret := make([]byte, 32)
if _, err = rand.Read(secret); err != nil {
return err
}
signer, err := storage.NewSigner(files, baseURL+"/file", secret)
if err != nil {
return err
}
// Production applications authorize the caller before this operation. The
// sample intentionally exposes only a generated fixture on loopback.
preview, err := signer.PresignGetWithOptions(context.Background(), "sample.pdf", 30*time.Minute, storage.ReadOptions{
Disposition: storage.DispositionInline, FileName: "示例报告.pdf", ContentType: "application/pdf",
})
if err != nil {
return err
}
download, err := signer.PresignGetWithOptions(context.Background(), "sample.pdf", 30*time.Minute, storage.ReadOptions{FileName: "示例报告.pdf"})
if err != nil {
return err
}
page := template.Must(template.New("page").Parse(`<!doctype html><html lang="zh-CN"><meta charset="utf-8"><title>Signed read example</title>
<body><h1>文件预览与下载</h1><p><a href="{{.Preview.URL}}">打开示例 PDF</a> · <a href="{{.Download.URL}}">下载示例 PDF</a></p>
<p>链接到期时间:{{.Preview.ExpiresAt}}</p><p>只读演示:无数据库、业务身份或公开磁盘目录。</p></body></html>`))
mux := http.NewServeMux()
mux.Handle("/file", signer)
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" || r.Method != http.MethodGet {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Cache-Control", "private, no-store")
w.Header().Set("Referrer-Policy", "no-referrer")
_ = page.Execute(w, struct{ Preview, Download storage.ReadLink }{preview, download})
})
server := &http.Server{Handler: mux, ReadHeaderTimeout: 5 * time.Second, IdleTimeout: 30 * time.Second}
done := make(chan error, 1)
go func() { done <- server.Serve(listener) }()
defer server.Close()
if check {
if err := checkHTTP(preview, download, content); err != nil {
return err
}
fmt.Println("signed read: PDF preview, Chinese filename, download, HEAD and Range passed")
return nil
}
// Never print signed URLs or enable request logging containing query strings.
fmt.Println("Open " + baseURL + " (Ctrl+C to stop)")
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
select {
case err := <-done:
if errors.Is(err, http.ErrServerClosed) {
return nil
}
return err
case <-ctx.Done():
shutdown, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return server.Shutdown(shutdown)
}
}
func checkHTTP(preview, download storage.ReadLink, content []byte) error {
client := &http.Client{Timeout: 5 * time.Second}
for _, tc := range []struct {
link storage.ReadLink
method, span, disposition string
status int
body []byte
}{
{preview, "GET", "", "inline", 200, content},
{preview, "HEAD", "", "inline", 200, nil},
{preview, "GET", "bytes=0-7", "inline", 206, content[:8]},
{download, "GET", "", "attachment", 200, content},
} {
req, err := http.NewRequest(tc.method, tc.link.URL, nil)
if err != nil {
return errors.New("invalid example link")
}
if tc.span != "" {
req.Header.Set("Range", tc.span)
}
response, err := client.Do(req)
if err != nil {
return errors.New("example request failed")
}
body, err := io.ReadAll(response.Body)
response.Body.Close()
kind, params, headerErr := mime.ParseMediaType(response.Header.Get("Content-Disposition"))
if err != nil || headerErr != nil || response.StatusCode != tc.status || !bytes.Equal(body, tc.body) || kind != tc.disposition || params["filename"] != "示例报告.pdf" || response.Header.Get("Content-Type") != "application/pdf" {
return errors.New("example response did not match signed options")
}
size := len(tc.body)
if tc.method == "HEAD" {
size = len(content)
}
if response.Header.Get("Content-Length") != strconv.Itoa(size) || response.Header.Get("Cache-Control") != "private, no-store" {
return errors.New("example size or cache policy mismatch")
}
}
return nil
}
// A complete, single-page PDF with a real cross-reference table. No generator
// dependency or business document is needed for this example.
func samplePDF() []byte {
stream := "BT /F1 24 Tf 72 720 Td (go-omni signed read) Tj ET\n"
objects := []string{
"<< /Type /Catalog /Pages 2 0 R >>",
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>",
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
fmt.Sprintf("<< /Length %d >>\nstream\n%sendstream", len(stream), stream),
}
var pdf bytes.Buffer
pdf.WriteString("%PDF-1.4\n")
offsets := []int{0}
for i, object := range objects {
offsets = append(offsets, pdf.Len())
fmt.Fprintf(&pdf, "%d 0 obj\n%s\nendobj\n", i+1, object)
}
xref := pdf.Len()
fmt.Fprintf(&pdf, "xref\n0 %d\n0000000000 65535 f \n", len(offsets))
for _, offset := range offsets[1:] {
fmt.Fprintf(&pdf, "%010d 00000 n \n", offset)
}
fmt.Fprintf(&pdf, "trailer\n<< /Size %d /Root 1 0 R >>\nstartxref\n%d\n%%%%EOF\n", len(offsets), xref)
return pdf.Bytes()
}