input validation (truncation mostly)
This commit is contained in:
@@ -27,6 +27,7 @@ func qrchFromRequest(r *http.Request) *qrbill.QRCH {
|
|||||||
CdtrInf: qrbill.QRCHCdtrInf{
|
CdtrInf: qrbill.QRCHCdtrInf{
|
||||||
IBAN: ifEmpty(r.FormValue("criban"), "CH0209000000870913543"),
|
IBAN: ifEmpty(r.FormValue("criban"), "CH0209000000870913543"),
|
||||||
Cdtr: qrbill.Address{
|
Cdtr: qrbill.Address{
|
||||||
|
// Must be structured address e.g. for ZKB mobile banking app
|
||||||
AdrTp: qrbill.AddressTypeStructured,
|
AdrTp: qrbill.AddressTypeStructured,
|
||||||
Name: ifEmpty(r.FormValue("crname"), "Legalize it!"),
|
Name: ifEmpty(r.FormValue("crname"), "Legalize it!"),
|
||||||
StrtNmOrAdrLine1: ifEmpty(r.FormValue("craddr1"), "Quellenstrasse 25"),
|
StrtNmOrAdrLine1: ifEmpty(r.FormValue("craddr1"), "Quellenstrasse 25"),
|
||||||
@@ -41,6 +42,7 @@ func qrchFromRequest(r *http.Request) *qrbill.QRCH {
|
|||||||
Ccy: "CHF",
|
Ccy: "CHF",
|
||||||
},
|
},
|
||||||
UltmtDbtr: qrbill.Address{
|
UltmtDbtr: qrbill.Address{
|
||||||
|
// Must be structured address e.g. for ZKB mobile banking app
|
||||||
AdrTp: qrbill.AddressTypeStructured,
|
AdrTp: qrbill.AddressTypeStructured,
|
||||||
Name: ifEmpty(r.FormValue("udname"), "Michael Stapelberg"),
|
Name: ifEmpty(r.FormValue("udname"), "Michael Stapelberg"),
|
||||||
StrtNmOrAdrLine1: ifEmpty(r.FormValue("udaddr1"), "Brahmsstrasse 21"),
|
StrtNmOrAdrLine1: ifEmpty(r.FormValue("udaddr1"), "Brahmsstrasse 21"),
|
||||||
@@ -128,7 +130,7 @@ func logic() error {
|
|||||||
|
|
||||||
case "txt":
|
case "txt":
|
||||||
w.Header().Add("Content-Type", "text/plain; charset=utf-8")
|
w.Header().Add("Content-Type", "text/plain; charset=utf-8")
|
||||||
spew.Fdump(w, qrch.Fill())
|
spew.Fdump(w, qrch.Validate())
|
||||||
|
|
||||||
case "html":
|
case "html":
|
||||||
debugHTML(w, r, prefix, qrch)
|
debugHTML(w, r, prefix, qrch)
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ func debugHTML(w http.ResponseWriter, r *http.Request, prefix string, qrch *qrbi
|
|||||||
}
|
}
|
||||||
fmt.Fprintf(w, `<div class="qrch"><h1>input</h1>%s</div>`, spew(qrch))
|
fmt.Fprintf(w, `<div class="qrch"><h1>input</h1>%s</div>`, spew(qrch))
|
||||||
|
|
||||||
fmt.Fprintf(w, `<div class="qrch"><h1>validated</h1>%s</div>`, spew(qrch.Fill()))
|
fmt.Fprintf(w, `<div class="qrch"><h1>validated</h1>%s</div>`, spew(qrch.Validate()))
|
||||||
|
|
||||||
r.URL.Path = "/qr"
|
r.URL.Path = "/qr"
|
||||||
v := r.URL.Query()
|
v := r.URL.Query()
|
||||||
|
|||||||
102
qrbill.go
102
qrbill.go
@@ -6,11 +6,16 @@
|
|||||||
//
|
//
|
||||||
// https://www.paymentstandards.ch/dam/downloads/ig-qr-bill-en.pdf (English)
|
// https://www.paymentstandards.ch/dam/downloads/ig-qr-bill-en.pdf (English)
|
||||||
// https://www.paymentstandards.ch/dam/downloads/ig-qr-bill-de.pdf (German)
|
// https://www.paymentstandards.ch/dam/downloads/ig-qr-bill-de.pdf (German)
|
||||||
|
//
|
||||||
|
// Note
|
||||||
|
//
|
||||||
|
// QRR and SCOR references are not yet implemented.
|
||||||
package qrbill
|
package qrbill
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"image"
|
"image"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/aaronarduino/goqrsvg"
|
"github.com/aaronarduino/goqrsvg"
|
||||||
@@ -25,9 +30,6 @@ import (
|
|||||||
// Oriented upon the Swiss Implementation Guidelines for Credit Transfers for
|
// Oriented upon the Swiss Implementation Guidelines for Credit Transfers for
|
||||||
// the ISO 20022 Customer Credit Transfer Initiation message (pain.001).
|
// the ISO 20022 Customer Credit Transfer Initiation message (pain.001).
|
||||||
|
|
||||||
// Section 4.2.1: Character set:
|
|
||||||
// UTF-8 should be used for encoding
|
|
||||||
|
|
||||||
// see also:
|
// see also:
|
||||||
// https://github.com/codebude/QRCoder/wiki/Advanced-usage---Payload-generators#317-swissqrcode-iso-20022
|
// https://github.com/codebude/QRCoder/wiki/Advanced-usage---Payload-generators#317-swissqrcode-iso-20022
|
||||||
|
|
||||||
@@ -43,6 +45,10 @@ const (
|
|||||||
|
|
||||||
// CodingType is the character set code. Fixed value.
|
// CodingType is the character set code. Fixed value.
|
||||||
CodingType = "1" // UTF-8 restricted to the Latin character set
|
CodingType = "1" // UTF-8 restricted to the Latin character set
|
||||||
|
|
||||||
|
// Trailer is an unambiguous indicator for the end of payment data. Fixed
|
||||||
|
// value.
|
||||||
|
Trailer = "EPD" // End Payment Data
|
||||||
)
|
)
|
||||||
|
|
||||||
// AddressType corresponds to AdrTp in ISO20022.
|
// AddressType corresponds to AdrTp in ISO20022.
|
||||||
@@ -53,9 +59,6 @@ const (
|
|||||||
AddressTypeCombined = "K"
|
AddressTypeCombined = "K"
|
||||||
)
|
)
|
||||||
|
|
||||||
// - fixed length: 21 alphanumeric characters
|
|
||||||
// - only IBANs with CH or LI country code permitted
|
|
||||||
|
|
||||||
type Address struct {
|
type Address struct {
|
||||||
AdrTp AddressType
|
AdrTp AddressType
|
||||||
Name string // Name, max 70. chars, first name + last name, or company name
|
Name string // Name, max 70. chars, first name + last name, or company name
|
||||||
@@ -66,6 +69,32 @@ type Address struct {
|
|||||||
Ctry string // Country, two-digit country code according to ISO 3166-1
|
Ctry string // Country, two-digit country code according to ISO 3166-1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a Address) Validate() Address {
|
||||||
|
c := a
|
||||||
|
|
||||||
|
if v := c.Name; len(v) > 70 {
|
||||||
|
c.Name = v[:70]
|
||||||
|
}
|
||||||
|
|
||||||
|
if v := c.StrtNmOrAdrLine1; len(v) > 70 {
|
||||||
|
c.StrtNmOrAdrLine1 = v[:70]
|
||||||
|
}
|
||||||
|
|
||||||
|
if v := c.BldgNbOrAdrLine2; len(v) > 16 {
|
||||||
|
c.BldgNbOrAdrLine2 = v[:16]
|
||||||
|
}
|
||||||
|
|
||||||
|
if v := c.PstCd; len(v) > 16 {
|
||||||
|
c.PstCd = v[:16]
|
||||||
|
}
|
||||||
|
|
||||||
|
if v := c.TwnNm; len(v) > 35 {
|
||||||
|
c.TwnNm = v[:35]
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
type QRCHHeader struct {
|
type QRCHHeader struct {
|
||||||
QRType string
|
QRType string
|
||||||
Version string
|
Version string
|
||||||
@@ -102,21 +131,68 @@ type QRCH struct {
|
|||||||
RmtInf QRCHRmtInf // Payment reference
|
RmtInf QRCHRmtInf // Payment reference
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *QRCH) Fill() *QRCH {
|
var (
|
||||||
|
nonNumericRe = regexp.MustCompile(`[^0-9]`)
|
||||||
|
nonAlphanumericRe = regexp.MustCompile(`[^A-Za-z0-9]`)
|
||||||
|
nonDecimalRe = regexp.MustCompile(`[^0-9.]`)
|
||||||
|
)
|
||||||
|
|
||||||
|
func (q *QRCH) Validate() *QRCH {
|
||||||
clone := &QRCH{}
|
clone := &QRCH{}
|
||||||
*clone = *q
|
*clone = *q
|
||||||
|
|
||||||
|
// Fill in all fixed values:
|
||||||
clone.Header.QRType = QRType
|
clone.Header.QRType = QRType
|
||||||
clone.Header.Version = Version
|
clone.Header.Version = Version
|
||||||
clone.Header.Coding = CodingType
|
clone.Header.Coding = CodingType
|
||||||
clone.RmtInf.AddInf.Trailer = "EPD" // TODO: constant
|
clone.RmtInf.AddInf.Trailer = Trailer
|
||||||
|
|
||||||
|
// TODO(spec): strictly speaking, we need to restrict ourselves only to
|
||||||
|
// permitted characters (see below). But, even the example from SIX does not
|
||||||
|
// do that (Monatspr_ä_mie):
|
||||||
|
// https://www.moneytoday.ch/lexikon/qr-rechnung/
|
||||||
|
|
||||||
|
// 4.3.2 Permitted characters
|
||||||
|
// general: only the latin character set is permitted. UTF-8 should be used for encoding
|
||||||
|
// numeric: 0-9
|
||||||
|
// alphanumeric: A-Z a-z 0-9
|
||||||
|
// decimal: 0-9 plus decimal separator .
|
||||||
|
|
||||||
|
// Character Set as per PAIN.001.001.03:
|
||||||
|
// https://businessbanking.bankofireland.com/app/uploads/2018/11/OMI015017-Credit-Transfer-PAIN.001.001.03-DIGITALFINAL-VERSION.pdf
|
||||||
|
|
||||||
|
// a b c d e f g h i j k l m n o p q r s t u v w x y z
|
||||||
|
// A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
|
||||||
|
// / - ? : ( ) . , ' +
|
||||||
|
// <space>
|
||||||
|
|
||||||
|
// Enforce all field constraints:
|
||||||
|
|
||||||
|
clone.CdtrInf.IBAN = nonAlphanumericRe.ReplaceAllString(clone.CdtrInf.IBAN, "")
|
||||||
|
|
||||||
|
clone.CdtrInf.Cdtr = clone.CdtrInf.Cdtr.Validate()
|
||||||
|
|
||||||
|
clone.UltmtCdtr = clone.UltmtCdtr.Validate()
|
||||||
|
|
||||||
|
clone.RmtInf.Tp = nonAlphanumericRe.ReplaceAllString(clone.RmtInf.Tp, "")
|
||||||
|
if v := clone.RmtInf.Tp; len(v) > 4 {
|
||||||
|
clone.RmtInf.Tp = v[:4]
|
||||||
|
}
|
||||||
|
|
||||||
|
clone.RmtInf.Ref = nonAlphanumericRe.ReplaceAllString(clone.RmtInf.Ref, "")
|
||||||
|
if v := clone.RmtInf.Ref; len(v) > 27 {
|
||||||
|
clone.RmtInf.Ref = v[:27]
|
||||||
|
}
|
||||||
|
|
||||||
|
if v := clone.RmtInf.AddInf.Ustrd; len(v) > 140 {
|
||||||
|
clone.RmtInf.AddInf.Ustrd = v[:140]
|
||||||
|
}
|
||||||
|
|
||||||
return clone
|
return clone
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *QRCH) Encode() (*Bill, error) {
|
func (q *QRCH) Encode() (*Bill, error) {
|
||||||
f := q.Fill()
|
f := q.Validate()
|
||||||
//f := q.Fill()
|
|
||||||
// TODO: data content must be no more than 997 characters
|
|
||||||
// TODO: truncate fields where necessary
|
|
||||||
return &Bill{
|
return &Bill{
|
||||||
qrcontents: strings.Join([]string{
|
qrcontents: strings.Join([]string{
|
||||||
f.Header.QRType,
|
f.Header.QRType,
|
||||||
@@ -155,7 +231,7 @@ func (q *QRCH) Encode() (*Bill, error) {
|
|||||||
f.RmtInf.Tp,
|
f.RmtInf.Tp,
|
||||||
f.RmtInf.Ref,
|
f.RmtInf.Ref,
|
||||||
f.RmtInf.AddInf.Ustrd,
|
f.RmtInf.AddInf.Ustrd,
|
||||||
"EPD", // TODO: constant
|
f.RmtInf.AddInf.Trailer,
|
||||||
}, "\n"),
|
}, "\n"),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user