From 8bf9d10964686c59ab2d3e1ec1092a468a6bed16 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Sat, 20 Jun 2020 09:39:06 +0200 Subject: [PATCH] implement web handler for QR code generation --- cmd/qrbill-example/example.go | 168 +++++++++++++++++++++++- go.mod | 1 + go.sum | 2 + qrbill.go | 235 +++++++++++----------------------- qrcodegenerator.go | 90 +++++++++++++ 5 files changed, 333 insertions(+), 163 deletions(-) create mode 100644 qrcodegenerator.go diff --git a/cmd/qrbill-example/example.go b/cmd/qrbill-example/example.go index a0c73a6..06d4042 100644 --- a/cmd/qrbill-example/example.go +++ b/cmd/qrbill-example/example.go @@ -1,14 +1,178 @@ package main import ( + "bytes" + "flag" + "fmt" + "image/png" + "io" "log" + "net/http" + "regexp" + "strings" + "github.com/davecgh/go-spew/spew" "github.com/stapelberg/qrbill" + + _ "net/http/pprof" ) +func ifEmpty(s, alternative string) string { + if s == "" { + return alternative + } + return s +} + +func qrchFromRequest(r *http.Request) *qrbill.QRCH { + return &qrbill.QRCH{ + CdtrInf: qrbill.QRCHCdtrInf{ + IBAN: ifEmpty(r.FormValue("criban"), "CH0209000000870913543"), + Cdtr: qrbill.Address{ + AdrTp: qrbill.AddressTypeStructured, + Name: ifEmpty(r.FormValue("crname"), "Legalize it!"), + StrtNmOrAdrLine1: ifEmpty(r.FormValue("craddr1"), "Quellenstrasse 25"), + BldgNbOrAdrLine2: ifEmpty(r.FormValue("craddr2"), ""), + PstCd: ifEmpty(r.FormValue("crpost"), "8005"), + TwnNm: ifEmpty(r.FormValue("crcity"), "Zürich"), + Ctry: ifEmpty(r.FormValue("crcountry"), "CH"), + }, + }, + CcyAmt: qrbill.QRCHCcyAmt{ + Amt: "", + Ccy: "CHF", + }, + UltmtDbtr: qrbill.Address{ + AdrTp: qrbill.AddressTypeStructured, + Name: ifEmpty(r.FormValue("udname"), "Michael Stapelberg"), + StrtNmOrAdrLine1: ifEmpty(r.FormValue("udaddr1"), "Brahmsstrasse 21"), + BldgNbOrAdrLine2: ifEmpty(r.FormValue("udaddr2"), ""), + PstCd: ifEmpty(r.FormValue("udpost"), "8003"), + TwnNm: ifEmpty(r.FormValue("udcity"), "Zürich"), + Ctry: ifEmpty(r.FormValue("udcountry"), "CH"), + }, + RmtInf: qrbill.QRCHRmtInf{ + Tp: "NON", // Reference type + Ref: "", // Reference + AddInf: qrbill.QRCHRmtInfAddInf{ + Ustrd: ifEmpty(r.FormValue("message"), "Spende 6141"), + }, + }, + } +} + +var fieldNameRe = regexp.MustCompile(`
( )*([^:]+):`) +var stringLiteralRe = regexp.MustCompile(`"([^"]*)"`) + +func qrHandler(format string) http.Handler { + if format != "png" && + format != "svg" && + format != "txt" && + format != "html" { + log.Fatalf("BUG: format must be either png, svg, txt or html") + } + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + prefix := "[" + r.RemoteAddr + "]" + log.Printf("%s handling request for %s", prefix, r.URL.Path) + defer log.Printf("%s done: %s", prefix, r.URL.Path) + + qrch := qrchFromRequest(r) + + bill, err := qrch.Encode() + if err != nil { + log.Print(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + var b []byte + switch format { + case "png": + code, err := bill.EncodeToImage() + if err != nil { + log.Print(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + var buf bytes.Buffer + if err := png.Encode(&buf, code); err != nil { + log.Print(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + b = buf.Bytes() + w.Header().Add("Content-Type", "image/png") + + case "svg": + var err error + b, err = bill.EncodeToSVG() + if err != nil { + log.Print(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Add("Content-Type", "image/svg+xml") + + case "txt": + w.Header().Add("Content-Type", "text/plain; charset=utf-8") + spew.Fdump(w, qrch.Fill()) + + case "html": + w.Header().Add("Content-Type", "text/html; charset=utf-8") + fmt.Fprintf(w, ` + + QR Bill HTML Debug Page + + + +`) + sp := spew.Sdump(qrch.Fill()) + sp = strings.ReplaceAll(sp, "\n", "
") + sp = strings.ReplaceAll(sp, " ", " ") + sp = stringLiteralRe.ReplaceAllStringFunc(sp, func(stringLiteral string) string { + return `` + stringLiteral + "" + }) + sp = fieldNameRe.ReplaceAllStringFunc(sp, func(fieldName string) string { + return `` + fieldName + "" + }) + fmt.Fprintf(w, "%s", sp) + } + + // TODO: add cache control headers + if _, err := io.Copy(w, bytes.NewReader(b)); err != nil { + log.Print(err) + return + } + }) +} + func logic() error { - return qrbill.Generate() - //return nil + var listen = flag.String("listen", "localhost:9933", "[host]:port to listen on") + flag.Parse() + http.Handle("/qr.png", qrHandler("png")) + http.Handle("/qr.svg", qrHandler("svg")) + http.Handle("/qr.txt", qrHandler("txt")) + http.Handle("/qr.html", qrHandler("html")) + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + http.Error(w, "not found", http.StatusNotFound) + return + } + w.Header().Add("Content-Type", "text/html; charset=utf-8") + // TODO: add explanation for how to construt a URL + // e.g. for usage in filemaker web view + fmt.Fprintf(w, "