PrintToVeraeTimeStamp/internal/netprint/stamp_api.go
Cursor Agent 927229ab4f
AirDrop stamped PDFs from the macOS menu bar.
The menu-bar app, not the root daemon, opens the AirDrop share sheet for each finished job when the setting is on. The unsigned arm64 package installs the daemon and LaunchAgent, and Darwin DNSServiceRegister advertises the _universal and _print subtypes.

Co-authored-by: George Lambert <marchon@gmail.com>
2026-10-05 01:27:23 +00:00

259 lines
6.8 KiB
Go

package netprint
import (
"archive/zip"
"bytes"
"fmt"
"io"
"log"
"mime"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"strings"
)
// handleStamp runs the print pipeline and returns the stamped PDF, or a zip
// of the original, the certificate, and the receipt JSON when format=zip.
func (s *Server) handleStamp(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
doc, title, format, err := readStampBody(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if len(doc) == 0 {
http.Error(w, "empty document", http.StatusBadRequest)
return
}
if len(doc) > maxDocument {
http.Error(w, "document is too large", http.StatusRequestEntityTooLarge)
return
}
id, err := s.store.AllocateID()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
user := stampUser(r.Header.Get("X-Verae-User"))
log.Printf("netprint: stamp api job %d title=%q bytes=%d", id, title, len(doc))
ProcessJob(s.store, s.snapshot(), s.printer, id, format, title, user, doc, s.stamp, s.forward, s.afterJob)
rec, ok := s.store.Get(id)
if !ok || rec.State != "completed" || rec.CertificatePDF == "" {
msg := "stamp failed"
if rec.Error != "" {
msg = rec.Error
}
http.Error(w, msg, http.StatusUnprocessableEntity)
return
}
cert, err := readStampFile(rec.CertificatePDF)
if err != nil {
http.Error(w, "stamped PDF is missing", http.StatusInternalServerError)
return
}
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("X-Verae-Receipt-Id", rec.ReceiptID)
w.Header().Set("X-Verae-SHA256", rec.SHA256)
w.Header().Set("X-Verae-Receipt-JSON", s.receiptJSONURL(r, id))
if strings.EqualFold(r.URL.Query().Get("format"), "zip") {
payload, err := stampZip(rec, cert)
if err != nil {
http.Error(w, "could not build zip", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/zip")
w.Header().Set("Content-Disposition", `attachment; filename="verae-stamp.zip"`)
_, _ = w.Write(payload)
return
}
w.Header().Set("Content-Type", "application/pdf")
w.Header().Set("Content-Disposition", `attachment; filename="certificate.pdf"`)
_, _ = w.Write(cert)
}
func (s *Server) receiptJSONURL(r *http.Request, id int) string {
host := r.Host
if host == "" {
host = strings.TrimPrefix(strings.TrimSuffix(s.receiptsURL(), "/receipts"), "http://")
}
return fmt.Sprintf("http://%s/jobs/%d/receipt.json", host, id)
}
func (s *Server) authorizeStamp(w http.ResponseWriter, r *http.Request) bool {
pin, err := s.receiptsPIN()
if err != nil {
log.Printf("netprint: stamp lock: %v", err)
http.Error(w, "stamp unavailable", http.StatusForbidden)
return false
}
if pin == "" || stampTokenOK(r, pin) {
return true
}
w.Header().Set("WWW-Authenticate", `Bearer realm="verae-stamp"`)
http.Error(w, "PIN required", http.StatusUnauthorized)
return false
}
func stampTokenOK(r *http.Request, pin string) bool {
if c, err := r.Cookie(receiptsCookie); err == nil && cookieMatches(c.Value, pin) {
return true
}
if access := strings.TrimSpace(r.URL.Query().Get("access")); access != "" && pinMatch(access, pin) {
return true
}
if h := strings.TrimSpace(r.Header.Get("X-Verae-Access")); h != "" && pinMatch(h, pin) {
return true
}
authz := strings.TrimSpace(r.Header.Get("Authorization"))
if len(authz) > 7 && strings.EqualFold(authz[:7], "bearer ") && pinMatch(strings.TrimSpace(authz[7:]), pin) {
return true
}
return false
}
func readStampBody(r *http.Request) (doc []byte, title, format string, err error) {
ct := r.Header.Get("Content-Type")
mediaType, _, _ := mime.ParseMediaType(ct)
if strings.HasPrefix(mediaType, "multipart/") {
r.Body = http.MaxBytesReader(nil, r.Body, maxDocument+1<<20)
mr, err := r.MultipartReader()
if err != nil {
return nil, "", "", fmt.Errorf("bad multipart body")
}
var part *multipart.Part
for {
p, err := mr.NextPart()
if err == io.EOF {
break
}
if err != nil {
return nil, "", "", fmt.Errorf("bad multipart body")
}
name := p.FormName()
if part == nil || name == "file" || name == "document" || name == "pdf" {
if part != nil {
part.Close()
}
part = p
if name == "file" || name == "document" || name == "pdf" {
break
}
} else {
p.Close()
}
}
if part == nil {
return nil, "", "", fmt.Errorf("multipart body has no file")
}
defer part.Close()
doc, err = io.ReadAll(io.LimitReader(part, maxDocument+1))
if err != nil {
return nil, "", "", fmt.Errorf("read document")
}
title = stampTitle(part.FileName())
format = part.Header.Get("Content-Type")
return doc, title, format, nil
}
doc, err = io.ReadAll(io.LimitReader(http.MaxBytesReader(nil, r.Body, maxDocument+1), maxDocument+1))
if err != nil {
return nil, "", "", fmt.Errorf("read document")
}
title = stampTitle(firstNonEmpty(r.Header.Get("X-Filename"), r.URL.Query().Get("filename"), filenameParam(r.Header.Get("Content-Disposition"))))
format = mediaType
return doc, title, format, nil
}
func filenameParam(v string) string {
_, params, err := mime.ParseMediaType(v)
if err != nil {
return ""
}
return params["filename"]
}
func stampTitle(name string) string {
name = strings.ReplaceAll(name, "\\", "/")
name = filepath.Base(name)
name = strings.Map(func(r rune) rune {
if r < 32 || r == '"' {
return -1
}
return r
}, name)
name = strings.TrimSpace(name)
if name == "" || name == "." || name == "/" {
return "Stamp"
}
return name
}
func stampUser(user string) string {
user = strings.TrimSpace(strings.Map(func(r rune) rune {
if r < 32 {
return -1
}
return r
}, user))
if user == "" {
return "stamp"
}
if len(user) > 80 {
user = user[:80]
}
return user
}
func readStampFile(path string) ([]byte, error) {
if strings.TrimSpace(path) == "" {
return nil, fmt.Errorf("missing")
}
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
return io.ReadAll(io.LimitReader(f, maxDocument+1))
}
func stampZip(rec Record, cert []byte) ([]byte, error) {
var buf bytes.Buffer
zw := zip.NewWriter(&buf)
add := func(name string, b []byte) error {
if len(b) == 0 {
return nil
}
w, err := zw.Create(name)
if err != nil {
return err
}
_, err = w.Write(b)
return err
}
origPath := rec.OriginalPDF
if origPath == "" {
origPath = filepath.Join(filepath.Dir(rec.CertificatePDF), "document.pdf")
}
orig, _ := readStampFile(origPath)
if err := add("original.pdf", orig); err != nil {
_ = zw.Close()
return nil, err
}
if err := add("certificate.pdf", cert); err != nil {
_ = zw.Close()
return nil, err
}
js, _ := readStampFile(rec.ReceiptJSON)
if err := add("receipt.json", js); err != nil {
_ = zw.Close()
return nil, err
}
if err := zw.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}