mirror of
https://github.com/mjl-/mox.git
synced 2024-12-26 16:33:47 +03:00
make setting up apple mail clients easier by providing .mobileconfig device management profiles
including showing a qr code to easily get the file on iphones. the profile is currently in the "account" page. idea by x8x in issue #65
This commit is contained in:
parent
a0f3856e40
commit
2b97c21f99
20 changed files with 2076 additions and 148 deletions
|
@ -29,8 +29,9 @@ See Quickstart below to get started.
|
||||||
(instructions to create DNS records, configure
|
(instructions to create DNS records, configure
|
||||||
SPF/DKIM/DMARC/TLSRPT/MTA-STS), for status information, managing
|
SPF/DKIM/DMARC/TLSRPT/MTA-STS), for status information, managing
|
||||||
accounts/domains, and modifying the configuration file.
|
accounts/domains, and modifying the configuration file.
|
||||||
- Autodiscovery (with SRV records, Microsoft-style and Thunderbird-style) for
|
- Autodiscovery (with SRV records, Microsoft-style, Thunderbird-style, and Apple
|
||||||
easy account setup (though not many clients support it).
|
device management profiles) for easy account setup (though client support is
|
||||||
|
limited).
|
||||||
- Webmail for reading/sending email from the browser.
|
- Webmail for reading/sending email from the browser.
|
||||||
- Webserver with serving static files and forwarding requests (reverse
|
- Webserver with serving static files and forwarding requests (reverse
|
||||||
proxy), so port 443 can also be used to serve websites.
|
proxy), so port 443 can also be used to serve websites.
|
||||||
|
@ -39,8 +40,8 @@ See Quickstart below to get started.
|
||||||
testing/developing, including pedantic mode.
|
testing/developing, including pedantic mode.
|
||||||
|
|
||||||
Mox is available under the MIT-license and was created by Mechiel Lukkien,
|
Mox is available under the MIT-license and was created by Mechiel Lukkien,
|
||||||
mechiel@ueber.net. Mox includes the Public Suffix List by Mozilla, under Mozilla
|
mechiel@ueber.net. Mox includes BSD-3-claused code from the Go Authors, and the
|
||||||
Public License, v2.0.
|
Public Suffix List by Mozilla under Mozilla Public License, v2.0.
|
||||||
|
|
||||||
|
|
||||||
# Download
|
# Download
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -15,6 +15,7 @@ require (
|
||||||
golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb
|
golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb
|
||||||
golang.org/x/net v0.15.0
|
golang.org/x/net v0.15.0
|
||||||
golang.org/x/text v0.13.0
|
golang.org/x/text v0.13.0
|
||||||
|
rsc.io/qr v0.2.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -510,5 +510,7 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt
|
||||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||||
|
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
|
||||||
|
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
|
||||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||||
|
|
227
http/autoconf.go
227
http/autoconf.go
|
@ -4,11 +4,12 @@ import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
"rsc.io/qr"
|
||||||
|
|
||||||
"github.com/mjl-/mox/config"
|
|
||||||
"github.com/mjl-/mox/mlog"
|
"github.com/mjl-/mox/mlog"
|
||||||
"github.com/mjl-/mox/mox-"
|
"github.com/mjl-/mox/mox-"
|
||||||
"github.com/mjl-/mox/smtp"
|
"github.com/mjl-/mox/smtp"
|
||||||
|
@ -36,7 +37,9 @@ var (
|
||||||
// - Thunderbird will request an "autoconfig" xml file.
|
// - Thunderbird will request an "autoconfig" xml file.
|
||||||
// - Microsoft tools will request an "autodiscovery" xml file.
|
// - Microsoft tools will request an "autodiscovery" xml file.
|
||||||
// - In my tests on an internal domain, iOS mail only talks to Apple servers, then
|
// - In my tests on an internal domain, iOS mail only talks to Apple servers, then
|
||||||
// does not attempt autoconfiguration. Possibly due to them being private DNS names.
|
// does not attempt autoconfiguration. Possibly due to them being private DNS
|
||||||
|
// names. Apple software can be provisioned with "mobileconfig" profile files,
|
||||||
|
// which users can download after logging in.
|
||||||
//
|
//
|
||||||
// DNS records seem optional, but autoconfig.<domain> and autodiscover.<domain>
|
// DNS records seem optional, but autoconfig.<domain> and autodiscover.<domain>
|
||||||
// (both CNAME or A) are useful, and so is SRV _autodiscovery._tcp.<domain> 0 0 443
|
// (both CNAME or A) are useful, and so is SRV _autodiscovery._tcp.<domain> 0 0 443
|
||||||
|
@ -67,13 +70,31 @@ func autoconfHandle(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := mox.Conf.Domain(addr.Domain); !ok {
|
socketType := func(tlsMode mox.TLSMode) (string, error) {
|
||||||
http.Error(w, "400 - bad request - unknown domain", http.StatusBadRequest)
|
switch tlsMode {
|
||||||
|
case mox.TLSModeImmediate:
|
||||||
|
return "SSL", nil
|
||||||
|
case mox.TLSModeSTARTTLS:
|
||||||
|
return "STARTTLS", nil
|
||||||
|
case mox.TLSModeNone:
|
||||||
|
return "plain", nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unknown tls mode %v", tlsMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var imapTLS, submissionTLS string
|
||||||
|
config, err := mox.ClientConfigDomain(addr.Domain)
|
||||||
|
if err == nil {
|
||||||
|
imapTLS, err = socketType(config.IMAP.TLSMode)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
submissionTLS, err = socketType(config.Submission.TLSMode)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "400 - bad request - "+err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
addrDom = addr.Domain.Name()
|
|
||||||
|
|
||||||
hostname := mox.Conf.Static.HostnameDomain
|
|
||||||
|
|
||||||
// Thunderbird doesn't seem to allow U-labels, always return ASCII names.
|
// Thunderbird doesn't seem to allow U-labels, always return ASCII names.
|
||||||
var resp autoconfigResponse
|
var resp autoconfigResponse
|
||||||
|
@ -83,64 +104,24 @@ func autoconfHandle(w http.ResponseWriter, r *http.Request) {
|
||||||
resp.EmailProvider.DisplayName = email
|
resp.EmailProvider.DisplayName = email
|
||||||
resp.EmailProvider.DisplayShortName = addr.Domain.ASCII
|
resp.EmailProvider.DisplayShortName = addr.Domain.ASCII
|
||||||
|
|
||||||
var imapPort int
|
|
||||||
var imapSocket string
|
|
||||||
for _, l := range mox.Conf.Static.Listeners {
|
|
||||||
if l.IMAPS.Enabled {
|
|
||||||
imapSocket = "SSL"
|
|
||||||
imapPort = config.Port(l.IMAPS.Port, 993)
|
|
||||||
} else if l.IMAP.Enabled {
|
|
||||||
if l.TLS != nil && imapSocket != "SSL" {
|
|
||||||
imapSocket = "STARTTLS"
|
|
||||||
imapPort = config.Port(l.IMAP.Port, 143)
|
|
||||||
} else if imapSocket == "" {
|
|
||||||
imapSocket = "plain"
|
|
||||||
imapPort = config.Port(l.IMAP.Port, 143)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if imapPort == 0 {
|
|
||||||
log.Error("autoconfig: no imap configured?")
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo: specify SCRAM-SHA-256 once thunderbird and autoconfig supports it. or perhaps that will fall under "password-encrypted" by then.
|
// todo: specify SCRAM-SHA-256 once thunderbird and autoconfig supports it. or perhaps that will fall under "password-encrypted" by then.
|
||||||
|
|
||||||
resp.EmailProvider.IncomingServer.Type = "imap"
|
resp.EmailProvider.IncomingServer.Type = "imap"
|
||||||
resp.EmailProvider.IncomingServer.Hostname = hostname.ASCII
|
resp.EmailProvider.IncomingServer.Hostname = config.IMAP.Host.ASCII
|
||||||
resp.EmailProvider.IncomingServer.Port = imapPort
|
resp.EmailProvider.IncomingServer.Port = config.IMAP.Port
|
||||||
resp.EmailProvider.IncomingServer.SocketType = imapSocket
|
resp.EmailProvider.IncomingServer.SocketType = imapTLS
|
||||||
resp.EmailProvider.IncomingServer.Username = email
|
resp.EmailProvider.IncomingServer.Username = email
|
||||||
resp.EmailProvider.IncomingServer.Authentication = "password-encrypted"
|
resp.EmailProvider.IncomingServer.Authentication = "password-encrypted"
|
||||||
|
|
||||||
var smtpPort int
|
|
||||||
var smtpSocket string
|
|
||||||
for _, l := range mox.Conf.Static.Listeners {
|
|
||||||
if l.Submissions.Enabled {
|
|
||||||
smtpSocket = "SSL"
|
|
||||||
smtpPort = config.Port(l.Submissions.Port, 465)
|
|
||||||
} else if l.Submission.Enabled {
|
|
||||||
if l.TLS != nil && smtpSocket != "SSL" {
|
|
||||||
smtpSocket = "STARTTLS"
|
|
||||||
smtpPort = config.Port(l.Submission.Port, 587)
|
|
||||||
} else if smtpSocket == "" {
|
|
||||||
smtpSocket = "plain"
|
|
||||||
smtpPort = config.Port(l.Submission.Port, 587)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if smtpPort == 0 {
|
|
||||||
log.Error("autoconfig: no smtp submission configured?")
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.EmailProvider.OutgoingServer.Type = "smtp"
|
resp.EmailProvider.OutgoingServer.Type = "smtp"
|
||||||
resp.EmailProvider.OutgoingServer.Hostname = hostname.ASCII
|
resp.EmailProvider.OutgoingServer.Hostname = config.Submission.Host.ASCII
|
||||||
resp.EmailProvider.OutgoingServer.Port = smtpPort
|
resp.EmailProvider.OutgoingServer.Port = config.Submission.Port
|
||||||
resp.EmailProvider.OutgoingServer.SocketType = smtpSocket
|
resp.EmailProvider.OutgoingServer.SocketType = submissionTLS
|
||||||
resp.EmailProvider.OutgoingServer.Username = email
|
resp.EmailProvider.OutgoingServer.Username = email
|
||||||
resp.EmailProvider.OutgoingServer.Authentication = "password-encrypted"
|
resp.EmailProvider.OutgoingServer.Authentication = "password-encrypted"
|
||||||
|
|
||||||
// todo: should we put the email address in the URL?
|
// todo: should we put the email address in the URL?
|
||||||
resp.ClientConfigUpdate.URL = fmt.Sprintf("https://%s/mail/config-v1.1.xml", hostname.ASCII)
|
resp.ClientConfigUpdate.URL = fmt.Sprintf("https://autoconfig.%s/mail/config-v1.1.xml", addr.Domain.ASCII)
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
||||||
enc := xml.NewEncoder(w)
|
enc := xml.NewEncoder(w)
|
||||||
|
@ -188,13 +169,33 @@ func autodiscoverHandle(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := mox.Conf.Domain(addr.Domain); !ok {
|
// tlsmode returns the "ssl" and "encryption" fields.
|
||||||
http.Error(w, "400 - bad request - unknown domain", http.StatusBadRequest)
|
tlsmode := func(tlsMode mox.TLSMode) (string, string, error) {
|
||||||
|
switch tlsMode {
|
||||||
|
case mox.TLSModeImmediate:
|
||||||
|
return "on", "TLS", nil
|
||||||
|
case mox.TLSModeSTARTTLS:
|
||||||
|
return "on", "", nil
|
||||||
|
case mox.TLSModeNone:
|
||||||
|
return "off", "", nil
|
||||||
|
default:
|
||||||
|
return "", "", fmt.Errorf("unknown tls mode %v", tlsMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var imapSSL, imapEncryption string
|
||||||
|
var submissionSSL, submissionEncryption string
|
||||||
|
config, err := mox.ClientConfigDomain(addr.Domain)
|
||||||
|
if err == nil {
|
||||||
|
imapSSL, imapEncryption, err = tlsmode(config.IMAP.TLSMode)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
submissionSSL, submissionEncryption, err = tlsmode(config.Submission.TLSMode)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "400 - bad request - "+err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
addrDom = addr.Domain.Name()
|
|
||||||
|
|
||||||
hostname := mox.Conf.Static.HostnameDomain
|
|
||||||
|
|
||||||
// The docs are generated and fragmented in many tiny pages, hard to follow.
|
// The docs are generated and fragmented in many tiny pages, hard to follow.
|
||||||
// High-level starting point, https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxdscli/78530279-d042-4eb0-a1f4-03b18143cd19
|
// High-level starting point, https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxdscli/78530279-d042-4eb0-a1f4-03b18143cd19
|
||||||
|
@ -205,47 +206,6 @@ func autodiscoverHandle(w http.ResponseWriter, r *http.Request) {
|
||||||
// use. See
|
// use. See
|
||||||
// https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxdscli/21fd2dd5-c4ee-485b-94fb-e7db5da93726
|
// https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxdscli/21fd2dd5-c4ee-485b-94fb-e7db5da93726
|
||||||
|
|
||||||
var imapPort int
|
|
||||||
imapSSL := "off"
|
|
||||||
var imapEncryption string
|
|
||||||
|
|
||||||
var smtpPort int
|
|
||||||
smtpSSL := "off"
|
|
||||||
var smtpEncryption string
|
|
||||||
for _, l := range mox.Conf.Static.Listeners {
|
|
||||||
if l.IMAPS.Enabled {
|
|
||||||
imapPort = config.Port(l.IMAPS.Port, 993)
|
|
||||||
imapSSL = "on"
|
|
||||||
imapEncryption = "TLS" // Assuming this means direct TLS.
|
|
||||||
} else if l.IMAP.Enabled {
|
|
||||||
if l.TLS != nil && imapEncryption != "TLS" {
|
|
||||||
imapSSL = "on"
|
|
||||||
imapPort = config.Port(l.IMAP.Port, 143)
|
|
||||||
} else if imapSSL == "" {
|
|
||||||
imapPort = config.Port(l.IMAP.Port, 143)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if l.Submissions.Enabled {
|
|
||||||
smtpPort = config.Port(l.Submissions.Port, 465)
|
|
||||||
smtpSSL = "on"
|
|
||||||
smtpEncryption = "TLS" // Assuming this means direct TLS.
|
|
||||||
} else if l.Submission.Enabled {
|
|
||||||
if l.TLS != nil && smtpEncryption != "TLS" {
|
|
||||||
smtpSSL = "on"
|
|
||||||
smtpPort = config.Port(l.Submission.Port, 587)
|
|
||||||
} else if smtpSSL == "" {
|
|
||||||
smtpPort = config.Port(l.Submission.Port, 587)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if imapPort == 0 {
|
|
||||||
log.Error("autoconfig: no smtp submission configured?")
|
|
||||||
}
|
|
||||||
if smtpPort == 0 {
|
|
||||||
log.Error("autoconfig: no imap configured?")
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
||||||
|
|
||||||
resp := autodiscoverResponse{}
|
resp := autodiscoverResponse{}
|
||||||
|
@ -259,8 +219,8 @@ func autodiscoverHandle(w http.ResponseWriter, r *http.Request) {
|
||||||
Protocol: []autodiscoverProtocol{
|
Protocol: []autodiscoverProtocol{
|
||||||
{
|
{
|
||||||
Type: "IMAP",
|
Type: "IMAP",
|
||||||
Server: hostname.ASCII,
|
Server: config.IMAP.Host.ASCII,
|
||||||
Port: imapPort,
|
Port: config.IMAP.Port,
|
||||||
LoginName: req.Request.EmailAddress,
|
LoginName: req.Request.EmailAddress,
|
||||||
SSL: imapSSL,
|
SSL: imapSSL,
|
||||||
Encryption: imapEncryption,
|
Encryption: imapEncryption,
|
||||||
|
@ -269,11 +229,11 @@ func autodiscoverHandle(w http.ResponseWriter, r *http.Request) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Type: "SMTP",
|
Type: "SMTP",
|
||||||
Server: hostname.ASCII,
|
Server: config.Submission.Host.ASCII,
|
||||||
Port: smtpPort,
|
Port: config.Submission.Port,
|
||||||
LoginName: req.Request.EmailAddress,
|
LoginName: req.Request.EmailAddress,
|
||||||
SSL: smtpSSL,
|
SSL: submissionSSL,
|
||||||
Encryption: smtpEncryption,
|
Encryption: submissionEncryption,
|
||||||
SPA: "off", // Override default "on", this is Microsofts proprietary authentication protocol.
|
SPA: "off", // Override default "on", this is Microsofts proprietary authentication protocol.
|
||||||
AuthRequired: "on",
|
AuthRequired: "on",
|
||||||
},
|
},
|
||||||
|
@ -360,3 +320,58 @@ type autodiscoverProtocol struct {
|
||||||
SPA string
|
SPA string
|
||||||
AuthRequired string
|
AuthRequired string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Serve a .mobileconfig file. This endpoint is not a standard place where Apple
|
||||||
|
// devices look. We point to it from the account page.
|
||||||
|
func mobileconfigHandle(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "GET" {
|
||||||
|
http.Error(w, "405 - method not allowed - get required", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
address := r.FormValue("address")
|
||||||
|
fullName := r.FormValue("name")
|
||||||
|
buf, err := MobileConfig(address, fullName)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "400 - bad request - "+err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h := w.Header()
|
||||||
|
filename := address
|
||||||
|
filename = strings.ReplaceAll(filename, ".", "-")
|
||||||
|
filename = strings.ReplaceAll(filename, "@", "-at-")
|
||||||
|
filename = "email-account-" + filename + ".mobileconfig"
|
||||||
|
h.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
|
||||||
|
w.Write(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve a png file with qrcode with the link to the .mobileconfig file, should be
|
||||||
|
// helpful for mobile devices.
|
||||||
|
func mobileconfigQRCodeHandle(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "GET" {
|
||||||
|
http.Error(w, "405 - method not allowed - get required", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(r.URL.Path, ".qrcode.png") {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compose URL, scheme and host are not set.
|
||||||
|
u := *r.URL
|
||||||
|
if r.TLS == nil {
|
||||||
|
u.Scheme = "http"
|
||||||
|
} else {
|
||||||
|
u.Scheme = "https"
|
||||||
|
}
|
||||||
|
u.Host = r.Host
|
||||||
|
u.Path = strings.TrimSuffix(u.Path, ".qrcode.png")
|
||||||
|
|
||||||
|
code, err := qr.Encode(u.String(), qr.L)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "500 - internal server error - generating qr-code: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h := w.Header()
|
||||||
|
h.Set("Content-Type", "image/png")
|
||||||
|
w.Write(code.PNG())
|
||||||
|
}
|
||||||
|
|
200
http/mobileconfig.go
Normal file
200
http/mobileconfig.go
Normal file
|
@ -0,0 +1,200 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/exp/maps"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
|
||||||
|
"github.com/mjl-/mox/mox-"
|
||||||
|
"github.com/mjl-/mox/smtp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Apple software isn't good at autoconfig/autodiscovery, but it can import a
|
||||||
|
// device management profile containing account settings.
|
||||||
|
//
|
||||||
|
// See https://developer.apple.com/documentation/devicemanagement/mail.
|
||||||
|
type deviceManagementProfile struct {
|
||||||
|
XMLName xml.Name `xml:"plist"`
|
||||||
|
Version string `xml:"version,attr"`
|
||||||
|
Dict dict `xml:"dict"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type array []dict
|
||||||
|
|
||||||
|
type dict map[string]any
|
||||||
|
|
||||||
|
// MarshalXML marshals as <dict> with multiple pairs of <key> and a value of various types.
|
||||||
|
func (m dict) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
// The plist format isn't that easy to generate with Go's xml package, it's leaving
|
||||||
|
// out reasonable structure, instead just concatenating key/value pairs. Perhaps
|
||||||
|
// there is a better way?
|
||||||
|
|
||||||
|
if err := e.EncodeToken(xml.StartElement{Name: xml.Name{Local: "dict"}}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
l := maps.Keys(m)
|
||||||
|
sort.Strings(l)
|
||||||
|
for _, k := range l {
|
||||||
|
tokens := []xml.Token{
|
||||||
|
xml.StartElement{Name: xml.Name{Local: "key"}},
|
||||||
|
xml.CharData([]byte(k)),
|
||||||
|
xml.EndElement{Name: xml.Name{Local: "key"}},
|
||||||
|
}
|
||||||
|
for _, t := range tokens {
|
||||||
|
if err := e.EncodeToken(t); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokens = nil
|
||||||
|
|
||||||
|
switch v := m[k].(type) {
|
||||||
|
case string:
|
||||||
|
tokens = []xml.Token{
|
||||||
|
xml.StartElement{Name: xml.Name{Local: "string"}},
|
||||||
|
xml.CharData([]byte(v)),
|
||||||
|
xml.EndElement{Name: xml.Name{Local: "string"}},
|
||||||
|
}
|
||||||
|
case int:
|
||||||
|
tokens = []xml.Token{
|
||||||
|
xml.StartElement{Name: xml.Name{Local: "integer"}},
|
||||||
|
xml.CharData([]byte(fmt.Sprintf("%d", v))),
|
||||||
|
xml.EndElement{Name: xml.Name{Local: "integer"}},
|
||||||
|
}
|
||||||
|
case bool:
|
||||||
|
tag := "false"
|
||||||
|
if v {
|
||||||
|
tag = "true"
|
||||||
|
}
|
||||||
|
tokens = []xml.Token{
|
||||||
|
xml.StartElement{Name: xml.Name{Local: tag}},
|
||||||
|
xml.EndElement{Name: xml.Name{Local: tag}},
|
||||||
|
}
|
||||||
|
case array:
|
||||||
|
if err := e.EncodeToken(xml.StartElement{Name: xml.Name{Local: "array"}}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, d := range v {
|
||||||
|
if err := d.MarshalXML(e, xml.StartElement{Name: xml.Name{Local: "array"}}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := e.EncodeToken(xml.EndElement{Name: xml.Name{Local: "array"}}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unexpected dict value of type %T", v)
|
||||||
|
}
|
||||||
|
for _, t := range tokens {
|
||||||
|
if err := e.EncodeToken(t); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := e.EncodeToken(xml.EndElement{Name: xml.Name{Local: "dict"}}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MobileConfig returns a device profile for a macOS Mail email account. The file
|
||||||
|
// should have a .mobileconfig extension. Opening the file adds it to Profiles in
|
||||||
|
// System Preferences, where it can be installed. This profile does not contain a
|
||||||
|
// password because sending opaque files containing passwords around to users seems
|
||||||
|
// like bad security practice.
|
||||||
|
//
|
||||||
|
// The config is not signed, so users must ignore warnings about unsigned profiles.
|
||||||
|
func MobileConfig(address, fullName string) ([]byte, error) {
|
||||||
|
addr, err := smtp.ParseAddress(address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing address: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := mox.ClientConfigDomain(addr.Domain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("getting config for domain: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apple software wants identifiers...
|
||||||
|
t := strings.Split(addr.Domain.Name(), ".")
|
||||||
|
slices.Reverse(t)
|
||||||
|
reverseAddr := strings.Join(t, ".") + "." + addr.Localpart.String()
|
||||||
|
|
||||||
|
// Apple software wants UUIDs... We generate them deterministically based on address
|
||||||
|
// and our code (through key, which we must change if code changes).
|
||||||
|
const key = "mox0"
|
||||||
|
uuid := func(prefix string) string {
|
||||||
|
mac := hmac.New(sha256.New, []byte(key))
|
||||||
|
mac.Write([]byte(prefix + "\n" + "\n" + address))
|
||||||
|
sum := mac.Sum(nil)
|
||||||
|
uuid := fmt.Sprintf("%x-%x-%x-%x-%x", sum[0:4], sum[4:6], sum[6:8], sum[8:10], sum[10:16])
|
||||||
|
return uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
uuidConfig := uuid("config")
|
||||||
|
uuidAccount := uuid("account")
|
||||||
|
|
||||||
|
// The "UseSSL" fields are underspecified in Apple's format. They say "If true,
|
||||||
|
// enables SSL for authentication on the incoming mail server.". I'm assuming they
|
||||||
|
// want to know if they should start immediately with a handshake, instead of
|
||||||
|
// starting out plain. There is no way to require STARTTLS though. You could even
|
||||||
|
// interpret their wording as this field enable authentication through client-side
|
||||||
|
// TLS certificates, given their "on the incoming mail server", instead of "of the
|
||||||
|
// incoming mail server".
|
||||||
|
|
||||||
|
var w bytes.Buffer
|
||||||
|
p := deviceManagementProfile{
|
||||||
|
Version: "1.0",
|
||||||
|
Dict: dict(map[string]any{
|
||||||
|
"PayloadDisplayName": fmt.Sprintf("%s email account", address),
|
||||||
|
"PayloadIdentifier": reverseAddr + ".email",
|
||||||
|
"PayloadType": "Configuration",
|
||||||
|
"PayloadUUID": uuidConfig,
|
||||||
|
"PayloadVersion": 1,
|
||||||
|
"PayloadContent": array{
|
||||||
|
dict(map[string]any{
|
||||||
|
"EmailAccountDescription": address,
|
||||||
|
"EmailAccountName": fullName,
|
||||||
|
"EmailAccountType": "EmailTypeIMAP",
|
||||||
|
"EmailAddress": address,
|
||||||
|
"IncomingMailServerAuthentication": "EmailAuthCRAMMD5", // SCRAM not an option at time of writing..
|
||||||
|
"IncomingMailServerUsername": address,
|
||||||
|
"IncomingMailServerHostName": config.IMAP.Host.ASCII,
|
||||||
|
"IncomingMailServerPortNumber": config.IMAP.Port,
|
||||||
|
"IncomingMailServerUseSSL": config.IMAP.TLSMode == mox.TLSModeImmediate,
|
||||||
|
"OutgoingMailServerAuthentication": "EmailAuthCRAMMD5", // SCRAM not an option at time of writing...
|
||||||
|
"OutgoingMailServerHostName": config.Submission.Host.ASCII,
|
||||||
|
"OutgoingMailServerPortNumber": config.Submission.Port,
|
||||||
|
"OutgoingMailServerUsername": address,
|
||||||
|
"OutgoingMailServerUseSSL": config.Submission.TLSMode == mox.TLSModeImmediate,
|
||||||
|
"OutgoingPasswordSameAsIncomingPassword": true,
|
||||||
|
"PayloadIdentifier": reverseAddr + ".email.account",
|
||||||
|
"PayloadType": "com.apple.mail.managed",
|
||||||
|
"PayloadUUID": uuidAccount,
|
||||||
|
"PayloadVersion": 1,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
if _, err := fmt.Fprint(&w, xml.Header); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := fmt.Fprint(&w, "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
enc := xml.NewEncoder(&w)
|
||||||
|
enc.Indent("", "\t")
|
||||||
|
if err := enc.Encode(p); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := fmt.Fprintln(&w); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return w.Bytes(), nil
|
||||||
|
}
|
12
http/web.go
12
http/web.go
|
@ -617,11 +617,19 @@ func Listen() {
|
||||||
port := config.Port(l.AutoconfigHTTPS.Port, 443)
|
port := config.Port(l.AutoconfigHTTPS.Port, 443)
|
||||||
srv := ensureServe(!l.AutoconfigHTTPS.NonTLS, port, "autoconfig-https")
|
srv := ensureServe(!l.AutoconfigHTTPS.NonTLS, port, "autoconfig-https")
|
||||||
autoconfigMatch := func(dom dns.Domain) bool {
|
autoconfigMatch := func(dom dns.Domain) bool {
|
||||||
// todo: may want to check this against the configured domains, could in theory be just a webserver.
|
// Thunderbird requests an autodiscovery URL at the email address domain name, so
|
||||||
return strings.HasPrefix(dom.ASCII, "autoconfig.")
|
// autoconfig prefix is optional.
|
||||||
|
if strings.HasPrefix(dom.ASCII, "autoconfig.") {
|
||||||
|
dom.ASCII = strings.TrimPrefix(dom.ASCII, "autoconfig.")
|
||||||
|
dom.Unicode = strings.TrimPrefix(dom.Unicode, "autoconfig.")
|
||||||
|
}
|
||||||
|
_, ok := mox.Conf.Domain(dom)
|
||||||
|
return ok
|
||||||
}
|
}
|
||||||
srv.Handle("autoconfig", autoconfigMatch, "/mail/config-v1.1.xml", safeHeaders(http.HandlerFunc(autoconfHandle)))
|
srv.Handle("autoconfig", autoconfigMatch, "/mail/config-v1.1.xml", safeHeaders(http.HandlerFunc(autoconfHandle)))
|
||||||
srv.Handle("autodiscover", autoconfigMatch, "/autodiscover/autodiscover.xml", safeHeaders(http.HandlerFunc(autodiscoverHandle)))
|
srv.Handle("autodiscover", autoconfigMatch, "/autodiscover/autodiscover.xml", safeHeaders(http.HandlerFunc(autodiscoverHandle)))
|
||||||
|
srv.Handle("mobileconfig", autoconfigMatch, "/profile.mobileconfig", safeHeaders(http.HandlerFunc(mobileconfigHandle)))
|
||||||
|
srv.Handle("mobileconfigqrcodepng", autoconfigMatch, "/profile.mobileconfig.qrcode.png", safeHeaders(http.HandlerFunc(mobileconfigQRCodeHandle)))
|
||||||
}
|
}
|
||||||
if l.MTASTSHTTPS.Enabled {
|
if l.MTASTSHTTPS.Enabled {
|
||||||
port := config.Port(l.MTASTSHTTPS.Port, 443)
|
port := config.Port(l.MTASTSHTTPS.Port, 443)
|
||||||
|
|
2
main.go
2
main.go
|
@ -509,7 +509,7 @@ configured over otherwise secured connections, like a VPN.
|
||||||
}
|
}
|
||||||
|
|
||||||
func printClientConfig(d dns.Domain) {
|
func printClientConfig(d dns.Domain) {
|
||||||
cc, err := mox.ClientConfigDomain(d)
|
cc, err := mox.ClientConfigsDomain(d)
|
||||||
xcheckf(err, "getting client config")
|
xcheckf(err, "getting client config")
|
||||||
fmt.Printf("%-20s %-30s %5s %-15s %s\n", "Protocol", "Host", "Port", "Listener", "Note")
|
fmt.Printf("%-20s %-30s %5s %-15s %s\n", "Protocol", "Host", "Port", "Listener", "Note")
|
||||||
for _, e := range cc.Entries {
|
for _, e := range cc.Entries {
|
||||||
|
|
116
mox-/admin.go
116
mox-/admin.go
|
@ -17,6 +17,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/exp/maps"
|
||||||
|
|
||||||
"github.com/mjl-/mox/config"
|
"github.com/mjl-/mox/config"
|
||||||
"github.com/mjl-/mox/dkim"
|
"github.com/mjl-/mox/dkim"
|
||||||
"github.com/mjl-/mox/dmarc"
|
"github.com/mjl-/mox/dmarc"
|
||||||
|
@ -590,8 +592,7 @@ func DomainRecords(domConf config.Domain, domain dns.Domain) ([]string, error) {
|
||||||
return records, nil
|
return records, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountAdd adds an account and an initial address and reloads the
|
// AccountAdd adds an account and an initial address and reloads the configuration.
|
||||||
// configuration.
|
|
||||||
//
|
//
|
||||||
// The new account does not have a password, so cannot yet log in. Email can be
|
// The new account does not have a password, so cannot yet log in. Email can be
|
||||||
// delivered.
|
// delivered.
|
||||||
|
@ -923,13 +924,96 @@ func AccountLimitsSave(ctx context.Context, account string, maxOutgoingMessagesP
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClientConfig holds the client configuration for IMAP/Submission for a
|
type TLSMode uint8
|
||||||
// domain.
|
|
||||||
type ClientConfig struct {
|
const (
|
||||||
Entries []ClientConfigEntry
|
TLSModeImmediate TLSMode = 0
|
||||||
|
TLSModeSTARTTLS TLSMode = 1
|
||||||
|
TLSModeNone TLSMode = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProtocolConfig struct {
|
||||||
|
Host dns.Domain
|
||||||
|
Port int
|
||||||
|
TLSMode TLSMode
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientConfigEntry struct {
|
type ClientConfig struct {
|
||||||
|
IMAP ProtocolConfig
|
||||||
|
Submission ProtocolConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientConfigDomain returns a single IMAP and Submission client configuration for
|
||||||
|
// a domain.
|
||||||
|
func ClientConfigDomain(d dns.Domain) (rconfig ClientConfig, rerr error) {
|
||||||
|
var haveIMAP, haveSubmission bool
|
||||||
|
|
||||||
|
if _, ok := Conf.Domain(d); !ok {
|
||||||
|
return ClientConfig{}, fmt.Errorf("unknown domain")
|
||||||
|
}
|
||||||
|
|
||||||
|
gather := func(l config.Listener) (done bool) {
|
||||||
|
host := Conf.Static.HostnameDomain
|
||||||
|
if l.Hostname != "" {
|
||||||
|
host = l.HostnameDomain
|
||||||
|
}
|
||||||
|
if !haveIMAP && l.IMAPS.Enabled {
|
||||||
|
rconfig.IMAP.Host = host
|
||||||
|
rconfig.IMAP.Port = config.Port(l.IMAPS.Port, 993)
|
||||||
|
rconfig.IMAP.TLSMode = TLSModeImmediate
|
||||||
|
haveIMAP = true
|
||||||
|
}
|
||||||
|
if !haveIMAP && l.IMAP.Enabled {
|
||||||
|
rconfig.IMAP.Host = host
|
||||||
|
rconfig.IMAP.Port = config.Port(l.IMAP.Port, 143)
|
||||||
|
rconfig.IMAP.TLSMode = TLSModeSTARTTLS
|
||||||
|
if l.TLS == nil {
|
||||||
|
rconfig.IMAP.TLSMode = TLSModeNone
|
||||||
|
}
|
||||||
|
haveIMAP = true
|
||||||
|
}
|
||||||
|
if !haveSubmission && l.Submissions.Enabled {
|
||||||
|
rconfig.Submission.Host = host
|
||||||
|
rconfig.Submission.Port = config.Port(l.Submissions.Port, 465)
|
||||||
|
rconfig.Submission.TLSMode = TLSModeImmediate
|
||||||
|
haveSubmission = true
|
||||||
|
}
|
||||||
|
if !haveSubmission && l.Submission.Enabled {
|
||||||
|
rconfig.Submission.Host = host
|
||||||
|
rconfig.Submission.Port = config.Port(l.Submission.Port, 587)
|
||||||
|
rconfig.Submission.TLSMode = TLSModeSTARTTLS
|
||||||
|
if l.TLS == nil {
|
||||||
|
rconfig.Submission.TLSMode = TLSModeNone
|
||||||
|
}
|
||||||
|
haveSubmission = true
|
||||||
|
}
|
||||||
|
return haveIMAP && haveSubmission
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look at the public listener first. Most likely the intended configuration.
|
||||||
|
if public, ok := Conf.Static.Listeners["public"]; ok {
|
||||||
|
if gather(public) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Go through the other listeners in consistent order.
|
||||||
|
names := maps.Keys(Conf.Static.Listeners)
|
||||||
|
sort.Strings(names)
|
||||||
|
for _, name := range names {
|
||||||
|
if gather(Conf.Static.Listeners[name]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ClientConfig{}, fmt.Errorf("no listeners found for imap and/or submission")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientConfigs holds the client configuration for IMAP/Submission for a
|
||||||
|
// domain.
|
||||||
|
type ClientConfigs struct {
|
||||||
|
Entries []ClientConfigsEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientConfigsEntry struct {
|
||||||
Protocol string
|
Protocol string
|
||||||
Host dns.Domain
|
Host dns.Domain
|
||||||
Port int
|
Port int
|
||||||
|
@ -937,16 +1021,16 @@ type ClientConfigEntry struct {
|
||||||
Note string
|
Note string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClientConfigDomain returns the client config for IMAP/Submission for a
|
// ClientConfigsDomain returns the client configs for IMAP/Submission for a
|
||||||
// domain.
|
// domain.
|
||||||
func ClientConfigDomain(d dns.Domain) (ClientConfig, error) {
|
func ClientConfigsDomain(d dns.Domain) (ClientConfigs, error) {
|
||||||
_, ok := Conf.Domain(d)
|
_, ok := Conf.Domain(d)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ClientConfig{}, fmt.Errorf("unknown domain")
|
return ClientConfigs{}, fmt.Errorf("unknown domain")
|
||||||
}
|
}
|
||||||
|
|
||||||
c := ClientConfig{}
|
c := ClientConfigs{}
|
||||||
c.Entries = []ClientConfigEntry{}
|
c.Entries = []ClientConfigsEntry{}
|
||||||
var listeners []string
|
var listeners []string
|
||||||
|
|
||||||
for name := range Conf.Static.Listeners {
|
for name := range Conf.Static.Listeners {
|
||||||
|
@ -973,16 +1057,16 @@ func ClientConfigDomain(d dns.Domain) (ClientConfig, error) {
|
||||||
host = l.HostnameDomain
|
host = l.HostnameDomain
|
||||||
}
|
}
|
||||||
if l.Submissions.Enabled {
|
if l.Submissions.Enabled {
|
||||||
c.Entries = append(c.Entries, ClientConfigEntry{"Submission (SMTP)", host, config.Port(l.Submissions.Port, 465), name, "with TLS"})
|
c.Entries = append(c.Entries, ClientConfigsEntry{"Submission (SMTP)", host, config.Port(l.Submissions.Port, 465), name, "with TLS"})
|
||||||
}
|
}
|
||||||
if l.IMAPS.Enabled {
|
if l.IMAPS.Enabled {
|
||||||
c.Entries = append(c.Entries, ClientConfigEntry{"IMAP", host, config.Port(l.IMAPS.Port, 993), name, "with TLS"})
|
c.Entries = append(c.Entries, ClientConfigsEntry{"IMAP", host, config.Port(l.IMAPS.Port, 993), name, "with TLS"})
|
||||||
}
|
}
|
||||||
if l.Submission.Enabled {
|
if l.Submission.Enabled {
|
||||||
c.Entries = append(c.Entries, ClientConfigEntry{"Submission (SMTP)", host, config.Port(l.Submission.Port, 587), name, note(l.TLS != nil, !l.Submission.NoRequireSTARTTLS)})
|
c.Entries = append(c.Entries, ClientConfigsEntry{"Submission (SMTP)", host, config.Port(l.Submission.Port, 587), name, note(l.TLS != nil, !l.Submission.NoRequireSTARTTLS)})
|
||||||
}
|
}
|
||||||
if l.IMAP.Enabled {
|
if l.IMAP.Enabled {
|
||||||
c.Entries = append(c.Entries, ClientConfigEntry{"IMAP", host, config.Port(l.IMAPS.Port, 143), name, note(l.TLS != nil, !l.IMAP.NoRequireSTARTTLS)})
|
c.Entries = append(c.Entries, ClientConfigsEntry{"IMAP", host, config.Port(l.IMAPS.Port, 143), name, note(l.TLS != nil, !l.IMAP.NoRequireSTARTTLS)})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
5
vendor/modules.txt
vendored
5
vendor/modules.txt
vendored
|
@ -156,3 +156,8 @@ google.golang.org/protobuf/runtime/protoiface
|
||||||
google.golang.org/protobuf/runtime/protoimpl
|
google.golang.org/protobuf/runtime/protoimpl
|
||||||
google.golang.org/protobuf/types/descriptorpb
|
google.golang.org/protobuf/types/descriptorpb
|
||||||
google.golang.org/protobuf/types/known/timestamppb
|
google.golang.org/protobuf/types/known/timestamppb
|
||||||
|
# rsc.io/qr v0.2.0
|
||||||
|
## explicit
|
||||||
|
rsc.io/qr
|
||||||
|
rsc.io/qr/coding
|
||||||
|
rsc.io/qr/gf256
|
||||||
|
|
27
vendor/rsc.io/qr/LICENSE
generated
vendored
Normal file
27
vendor/rsc.io/qr/LICENSE
generated
vendored
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above
|
||||||
|
copyright notice, this list of conditions and the following disclaimer
|
||||||
|
in the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
* Neither the name of Google Inc. nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
3
vendor/rsc.io/qr/README.md
generated
vendored
Normal file
3
vendor/rsc.io/qr/README.md
generated
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
Basic QR encoder.
|
||||||
|
|
||||||
|
go get [-u] rsc.io/qr
|
815
vendor/rsc.io/qr/coding/qr.go
generated
vendored
Normal file
815
vendor/rsc.io/qr/coding/qr.go
generated
vendored
Normal file
|
@ -0,0 +1,815 @@
|
||||||
|
// Copyright 2011 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package coding implements low-level QR coding details.
|
||||||
|
package coding // import "rsc.io/qr/coding"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"rsc.io/qr/gf256"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Field is the field for QR error correction.
|
||||||
|
var Field = gf256.NewField(0x11d, 2)
|
||||||
|
|
||||||
|
// A Version represents a QR version.
|
||||||
|
// The version specifies the size of the QR code:
|
||||||
|
// a QR code with version v has 4v+17 pixels on a side.
|
||||||
|
// Versions number from 1 to 40: the larger the version,
|
||||||
|
// the more information the code can store.
|
||||||
|
type Version int
|
||||||
|
|
||||||
|
const MinVersion = 1
|
||||||
|
const MaxVersion = 40
|
||||||
|
|
||||||
|
func (v Version) String() string {
|
||||||
|
return strconv.Itoa(int(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Version) sizeClass() int {
|
||||||
|
if v <= 9 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if v <= 26 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// DataBytes returns the number of data bytes that can be
|
||||||
|
// stored in a QR code with the given version and level.
|
||||||
|
func (v Version) DataBytes(l Level) int {
|
||||||
|
vt := &vtab[v]
|
||||||
|
lev := &vt.level[l]
|
||||||
|
return vt.bytes - lev.nblock*lev.check
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encoding implements a QR data encoding scheme.
|
||||||
|
// The implementations--Numeric, Alphanumeric, and String--specify
|
||||||
|
// the character set and the mapping from UTF-8 to code bits.
|
||||||
|
// The more restrictive the mode, the fewer code bits are needed.
|
||||||
|
type Encoding interface {
|
||||||
|
Check() error
|
||||||
|
Bits(v Version) int
|
||||||
|
Encode(b *Bits, v Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Bits struct {
|
||||||
|
b []byte
|
||||||
|
nbit int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bits) Reset() {
|
||||||
|
b.b = b.b[:0]
|
||||||
|
b.nbit = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bits) Bits() int {
|
||||||
|
return b.nbit
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bits) Bytes() []byte {
|
||||||
|
if b.nbit%8 != 0 {
|
||||||
|
panic("fractional byte")
|
||||||
|
}
|
||||||
|
return b.b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bits) Append(p []byte) {
|
||||||
|
if b.nbit%8 != 0 {
|
||||||
|
panic("fractional byte")
|
||||||
|
}
|
||||||
|
b.b = append(b.b, p...)
|
||||||
|
b.nbit += 8 * len(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bits) Write(v uint, nbit int) {
|
||||||
|
for nbit > 0 {
|
||||||
|
n := nbit
|
||||||
|
if n > 8 {
|
||||||
|
n = 8
|
||||||
|
}
|
||||||
|
if b.nbit%8 == 0 {
|
||||||
|
b.b = append(b.b, 0)
|
||||||
|
} else {
|
||||||
|
m := -b.nbit & 7
|
||||||
|
if n > m {
|
||||||
|
n = m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.nbit += n
|
||||||
|
sh := uint(nbit - n)
|
||||||
|
b.b[len(b.b)-1] |= uint8(v >> sh << uint(-b.nbit&7))
|
||||||
|
v -= v >> sh << sh
|
||||||
|
nbit -= n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Num is the encoding for numeric data.
|
||||||
|
// The only valid characters are the decimal digits 0 through 9.
|
||||||
|
type Num string
|
||||||
|
|
||||||
|
func (s Num) String() string {
|
||||||
|
return fmt.Sprintf("Num(%#q)", string(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Num) Check() error {
|
||||||
|
for _, c := range s {
|
||||||
|
if c < '0' || '9' < c {
|
||||||
|
return fmt.Errorf("non-numeric string %#q", string(s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var numLen = [3]int{10, 12, 14}
|
||||||
|
|
||||||
|
func (s Num) Bits(v Version) int {
|
||||||
|
return 4 + numLen[v.sizeClass()] + (10*len(s)+2)/3
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Num) Encode(b *Bits, v Version) {
|
||||||
|
b.Write(1, 4)
|
||||||
|
b.Write(uint(len(s)), numLen[v.sizeClass()])
|
||||||
|
var i int
|
||||||
|
for i = 0; i+3 <= len(s); i += 3 {
|
||||||
|
w := uint(s[i]-'0')*100 + uint(s[i+1]-'0')*10 + uint(s[i+2]-'0')
|
||||||
|
b.Write(w, 10)
|
||||||
|
}
|
||||||
|
switch len(s) - i {
|
||||||
|
case 1:
|
||||||
|
w := uint(s[i] - '0')
|
||||||
|
b.Write(w, 4)
|
||||||
|
case 2:
|
||||||
|
w := uint(s[i]-'0')*10 + uint(s[i+1]-'0')
|
||||||
|
b.Write(w, 7)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alpha is the encoding for alphanumeric data.
|
||||||
|
// The valid characters are 0-9A-Z$%*+-./: and space.
|
||||||
|
type Alpha string
|
||||||
|
|
||||||
|
const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"
|
||||||
|
|
||||||
|
func (s Alpha) String() string {
|
||||||
|
return fmt.Sprintf("Alpha(%#q)", string(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Alpha) Check() error {
|
||||||
|
for _, c := range s {
|
||||||
|
if strings.IndexRune(alphabet, c) < 0 {
|
||||||
|
return fmt.Errorf("non-alphanumeric string %#q", string(s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var alphaLen = [3]int{9, 11, 13}
|
||||||
|
|
||||||
|
func (s Alpha) Bits(v Version) int {
|
||||||
|
return 4 + alphaLen[v.sizeClass()] + (11*len(s)+1)/2
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Alpha) Encode(b *Bits, v Version) {
|
||||||
|
b.Write(2, 4)
|
||||||
|
b.Write(uint(len(s)), alphaLen[v.sizeClass()])
|
||||||
|
var i int
|
||||||
|
for i = 0; i+2 <= len(s); i += 2 {
|
||||||
|
w := uint(strings.IndexRune(alphabet, rune(s[i])))*45 +
|
||||||
|
uint(strings.IndexRune(alphabet, rune(s[i+1])))
|
||||||
|
b.Write(w, 11)
|
||||||
|
}
|
||||||
|
|
||||||
|
if i < len(s) {
|
||||||
|
w := uint(strings.IndexRune(alphabet, rune(s[i])))
|
||||||
|
b.Write(w, 6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String is the encoding for 8-bit data. All bytes are valid.
|
||||||
|
type String string
|
||||||
|
|
||||||
|
func (s String) String() string {
|
||||||
|
return fmt.Sprintf("String(%#q)", string(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s String) Check() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var stringLen = [3]int{8, 16, 16}
|
||||||
|
|
||||||
|
func (s String) Bits(v Version) int {
|
||||||
|
return 4 + stringLen[v.sizeClass()] + 8*len(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s String) Encode(b *Bits, v Version) {
|
||||||
|
b.Write(4, 4)
|
||||||
|
b.Write(uint(len(s)), stringLen[v.sizeClass()])
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
b.Write(uint(s[i]), 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Pixel describes a single pixel in a QR code.
|
||||||
|
type Pixel uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
Black Pixel = 1 << iota
|
||||||
|
Invert
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p Pixel) Offset() uint {
|
||||||
|
return uint(p >> 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
func OffsetPixel(o uint) Pixel {
|
||||||
|
return Pixel(o << 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r PixelRole) Pixel() Pixel {
|
||||||
|
return Pixel(r << 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Pixel) Role() PixelRole {
|
||||||
|
return PixelRole(p>>2) & 15
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Pixel) String() string {
|
||||||
|
s := p.Role().String()
|
||||||
|
if p&Black != 0 {
|
||||||
|
s += "+black"
|
||||||
|
}
|
||||||
|
if p&Invert != 0 {
|
||||||
|
s += "+invert"
|
||||||
|
}
|
||||||
|
s += "+" + strconv.FormatUint(uint64(p.Offset()), 10)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// A PixelRole describes the role of a QR pixel.
|
||||||
|
type PixelRole uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
_ PixelRole = iota
|
||||||
|
Position // position squares (large)
|
||||||
|
Alignment // alignment squares (small)
|
||||||
|
Timing // timing strip between position squares
|
||||||
|
Format // format metadata
|
||||||
|
PVersion // version pattern
|
||||||
|
Unused // unused pixel
|
||||||
|
Data // data bit
|
||||||
|
Check // error correction check bit
|
||||||
|
Extra
|
||||||
|
)
|
||||||
|
|
||||||
|
var roles = []string{
|
||||||
|
"",
|
||||||
|
"position",
|
||||||
|
"alignment",
|
||||||
|
"timing",
|
||||||
|
"format",
|
||||||
|
"pversion",
|
||||||
|
"unused",
|
||||||
|
"data",
|
||||||
|
"check",
|
||||||
|
"extra",
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r PixelRole) String() string {
|
||||||
|
if Position <= r && r <= Check {
|
||||||
|
return roles[r]
|
||||||
|
}
|
||||||
|
return strconv.Itoa(int(r))
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Level represents a QR error correction level.
|
||||||
|
// From least to most tolerant of errors, they are L, M, Q, H.
|
||||||
|
type Level int
|
||||||
|
|
||||||
|
const (
|
||||||
|
L Level = iota
|
||||||
|
M
|
||||||
|
Q
|
||||||
|
H
|
||||||
|
)
|
||||||
|
|
||||||
|
func (l Level) String() string {
|
||||||
|
if L <= l && l <= H {
|
||||||
|
return "LMQH"[l : l+1]
|
||||||
|
}
|
||||||
|
return strconv.Itoa(int(l))
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Code is a square pixel grid.
|
||||||
|
type Code struct {
|
||||||
|
Bitmap []byte // 1 is black, 0 is white
|
||||||
|
Size int // number of pixels on a side
|
||||||
|
Stride int // number of bytes per row
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Code) Black(x, y int) bool {
|
||||||
|
return 0 <= x && x < c.Size && 0 <= y && y < c.Size &&
|
||||||
|
c.Bitmap[y*c.Stride+x/8]&(1<<uint(7-x&7)) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Mask describes a mask that is applied to the QR
|
||||||
|
// code to avoid QR artifacts being interpreted as
|
||||||
|
// alignment and timing patterns (such as the squares
|
||||||
|
// in the corners). Valid masks are integers from 0 to 7.
|
||||||
|
type Mask int
|
||||||
|
|
||||||
|
// http://www.swetake.com/qr/qr5_en.html
|
||||||
|
var mfunc = []func(int, int) bool{
|
||||||
|
func(i, j int) bool { return (i+j)%2 == 0 },
|
||||||
|
func(i, j int) bool { return i%2 == 0 },
|
||||||
|
func(i, j int) bool { return j%3 == 0 },
|
||||||
|
func(i, j int) bool { return (i+j)%3 == 0 },
|
||||||
|
func(i, j int) bool { return (i/2+j/3)%2 == 0 },
|
||||||
|
func(i, j int) bool { return i*j%2+i*j%3 == 0 },
|
||||||
|
func(i, j int) bool { return (i*j%2+i*j%3)%2 == 0 },
|
||||||
|
func(i, j int) bool { return (i*j%3+(i+j)%2)%2 == 0 },
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Mask) Invert(y, x int) bool {
|
||||||
|
if m < 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return mfunc[m](y, x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Plan describes how to construct a QR code
|
||||||
|
// with a specific version, level, and mask.
|
||||||
|
type Plan struct {
|
||||||
|
Version Version
|
||||||
|
Level Level
|
||||||
|
Mask Mask
|
||||||
|
|
||||||
|
DataBytes int // number of data bytes
|
||||||
|
CheckBytes int // number of error correcting (checksum) bytes
|
||||||
|
Blocks int // number of data blocks
|
||||||
|
|
||||||
|
Pixel [][]Pixel // pixel map
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPlan returns a Plan for a QR code with the given
|
||||||
|
// version, level, and mask.
|
||||||
|
func NewPlan(version Version, level Level, mask Mask) (*Plan, error) {
|
||||||
|
p, err := vplan(version)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := fplan(level, mask, p); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := lplan(version, level, p); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := mplan(mask, p); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bits) Pad(n int) {
|
||||||
|
if n < 0 {
|
||||||
|
panic("qr: invalid pad size")
|
||||||
|
}
|
||||||
|
if n <= 4 {
|
||||||
|
b.Write(0, n)
|
||||||
|
} else {
|
||||||
|
b.Write(0, 4)
|
||||||
|
n -= 4
|
||||||
|
n -= -b.Bits() & 7
|
||||||
|
b.Write(0, -b.Bits()&7)
|
||||||
|
pad := n / 8
|
||||||
|
for i := 0; i < pad; i += 2 {
|
||||||
|
b.Write(0xec, 8)
|
||||||
|
if i+1 >= pad {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
b.Write(0x11, 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bits) AddCheckBytes(v Version, l Level) {
|
||||||
|
nd := v.DataBytes(l)
|
||||||
|
if b.nbit < nd*8 {
|
||||||
|
b.Pad(nd*8 - b.nbit)
|
||||||
|
}
|
||||||
|
if b.nbit != nd*8 {
|
||||||
|
panic("qr: too much data")
|
||||||
|
}
|
||||||
|
|
||||||
|
dat := b.Bytes()
|
||||||
|
vt := &vtab[v]
|
||||||
|
lev := &vt.level[l]
|
||||||
|
db := nd / lev.nblock
|
||||||
|
extra := nd % lev.nblock
|
||||||
|
chk := make([]byte, lev.check)
|
||||||
|
rs := gf256.NewRSEncoder(Field, lev.check)
|
||||||
|
for i := 0; i < lev.nblock; i++ {
|
||||||
|
if i == lev.nblock-extra {
|
||||||
|
db++
|
||||||
|
}
|
||||||
|
rs.ECC(dat[:db], chk)
|
||||||
|
b.Append(chk)
|
||||||
|
dat = dat[db:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b.Bytes()) != vt.bytes {
|
||||||
|
panic("qr: internal error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Plan) Encode(text ...Encoding) (*Code, error) {
|
||||||
|
var b Bits
|
||||||
|
for _, t := range text {
|
||||||
|
if err := t.Check(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
t.Encode(&b, p.Version)
|
||||||
|
}
|
||||||
|
if b.Bits() > p.DataBytes*8 {
|
||||||
|
return nil, fmt.Errorf("cannot encode %d bits into %d-bit code", b.Bits(), p.DataBytes*8)
|
||||||
|
}
|
||||||
|
b.AddCheckBytes(p.Version, p.Level)
|
||||||
|
bytes := b.Bytes()
|
||||||
|
|
||||||
|
// Now we have the checksum bytes and the data bytes.
|
||||||
|
// Construct the actual code.
|
||||||
|
c := &Code{Size: len(p.Pixel), Stride: (len(p.Pixel) + 7) &^ 7}
|
||||||
|
c.Bitmap = make([]byte, c.Stride*c.Size)
|
||||||
|
crow := c.Bitmap
|
||||||
|
for _, row := range p.Pixel {
|
||||||
|
for x, pix := range row {
|
||||||
|
switch pix.Role() {
|
||||||
|
case Data, Check:
|
||||||
|
o := pix.Offset()
|
||||||
|
if bytes[o/8]&(1<<uint(7-o&7)) != 0 {
|
||||||
|
pix ^= Black
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pix&Black != 0 {
|
||||||
|
crow[x/8] |= 1 << uint(7-x&7)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crow = crow[c.Stride:]
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// A version describes metadata associated with a version.
|
||||||
|
type version struct {
|
||||||
|
apos int
|
||||||
|
astride int
|
||||||
|
bytes int
|
||||||
|
pattern int
|
||||||
|
level [4]level
|
||||||
|
}
|
||||||
|
|
||||||
|
type level struct {
|
||||||
|
nblock int
|
||||||
|
check int
|
||||||
|
}
|
||||||
|
|
||||||
|
var vtab = []version{
|
||||||
|
{},
|
||||||
|
{100, 100, 26, 0x0, [4]level{{1, 7}, {1, 10}, {1, 13}, {1, 17}}}, // 1
|
||||||
|
{16, 100, 44, 0x0, [4]level{{1, 10}, {1, 16}, {1, 22}, {1, 28}}}, // 2
|
||||||
|
{20, 100, 70, 0x0, [4]level{{1, 15}, {1, 26}, {2, 18}, {2, 22}}}, // 3
|
||||||
|
{24, 100, 100, 0x0, [4]level{{1, 20}, {2, 18}, {2, 26}, {4, 16}}}, // 4
|
||||||
|
{28, 100, 134, 0x0, [4]level{{1, 26}, {2, 24}, {4, 18}, {4, 22}}}, // 5
|
||||||
|
{32, 100, 172, 0x0, [4]level{{2, 18}, {4, 16}, {4, 24}, {4, 28}}}, // 6
|
||||||
|
{20, 16, 196, 0x7c94, [4]level{{2, 20}, {4, 18}, {6, 18}, {5, 26}}}, // 7
|
||||||
|
{22, 18, 242, 0x85bc, [4]level{{2, 24}, {4, 22}, {6, 22}, {6, 26}}}, // 8
|
||||||
|
{24, 20, 292, 0x9a99, [4]level{{2, 30}, {5, 22}, {8, 20}, {8, 24}}}, // 9
|
||||||
|
{26, 22, 346, 0xa4d3, [4]level{{4, 18}, {5, 26}, {8, 24}, {8, 28}}}, // 10
|
||||||
|
{28, 24, 404, 0xbbf6, [4]level{{4, 20}, {5, 30}, {8, 28}, {11, 24}}}, // 11
|
||||||
|
{30, 26, 466, 0xc762, [4]level{{4, 24}, {8, 22}, {10, 26}, {11, 28}}}, // 12
|
||||||
|
{32, 28, 532, 0xd847, [4]level{{4, 26}, {9, 22}, {12, 24}, {16, 22}}}, // 13
|
||||||
|
{24, 20, 581, 0xe60d, [4]level{{4, 30}, {9, 24}, {16, 20}, {16, 24}}}, // 14
|
||||||
|
{24, 22, 655, 0xf928, [4]level{{6, 22}, {10, 24}, {12, 30}, {18, 24}}}, // 15
|
||||||
|
{24, 24, 733, 0x10b78, [4]level{{6, 24}, {10, 28}, {17, 24}, {16, 30}}}, // 16
|
||||||
|
{28, 24, 815, 0x1145d, [4]level{{6, 28}, {11, 28}, {16, 28}, {19, 28}}}, // 17
|
||||||
|
{28, 26, 901, 0x12a17, [4]level{{6, 30}, {13, 26}, {18, 28}, {21, 28}}}, // 18
|
||||||
|
{28, 28, 991, 0x13532, [4]level{{7, 28}, {14, 26}, {21, 26}, {25, 26}}}, // 19
|
||||||
|
{32, 28, 1085, 0x149a6, [4]level{{8, 28}, {16, 26}, {20, 30}, {25, 28}}}, // 20
|
||||||
|
{26, 22, 1156, 0x15683, [4]level{{8, 28}, {17, 26}, {23, 28}, {25, 30}}}, // 21
|
||||||
|
{24, 24, 1258, 0x168c9, [4]level{{9, 28}, {17, 28}, {23, 30}, {34, 24}}}, // 22
|
||||||
|
{28, 24, 1364, 0x177ec, [4]level{{9, 30}, {18, 28}, {25, 30}, {30, 30}}}, // 23
|
||||||
|
{26, 26, 1474, 0x18ec4, [4]level{{10, 30}, {20, 28}, {27, 30}, {32, 30}}}, // 24
|
||||||
|
{30, 26, 1588, 0x191e1, [4]level{{12, 26}, {21, 28}, {29, 30}, {35, 30}}}, // 25
|
||||||
|
{28, 28, 1706, 0x1afab, [4]level{{12, 28}, {23, 28}, {34, 28}, {37, 30}}}, // 26
|
||||||
|
{32, 28, 1828, 0x1b08e, [4]level{{12, 30}, {25, 28}, {34, 30}, {40, 30}}}, // 27
|
||||||
|
{24, 24, 1921, 0x1cc1a, [4]level{{13, 30}, {26, 28}, {35, 30}, {42, 30}}}, // 28
|
||||||
|
{28, 24, 2051, 0x1d33f, [4]level{{14, 30}, {28, 28}, {38, 30}, {45, 30}}}, // 29
|
||||||
|
{24, 26, 2185, 0x1ed75, [4]level{{15, 30}, {29, 28}, {40, 30}, {48, 30}}}, // 30
|
||||||
|
{28, 26, 2323, 0x1f250, [4]level{{16, 30}, {31, 28}, {43, 30}, {51, 30}}}, // 31
|
||||||
|
{32, 26, 2465, 0x209d5, [4]level{{17, 30}, {33, 28}, {45, 30}, {54, 30}}}, // 32
|
||||||
|
{28, 28, 2611, 0x216f0, [4]level{{18, 30}, {35, 28}, {48, 30}, {57, 30}}}, // 33
|
||||||
|
{32, 28, 2761, 0x228ba, [4]level{{19, 30}, {37, 28}, {51, 30}, {60, 30}}}, // 34
|
||||||
|
{28, 24, 2876, 0x2379f, [4]level{{19, 30}, {38, 28}, {53, 30}, {63, 30}}}, // 35
|
||||||
|
{22, 26, 3034, 0x24b0b, [4]level{{20, 30}, {40, 28}, {56, 30}, {66, 30}}}, // 36
|
||||||
|
{26, 26, 3196, 0x2542e, [4]level{{21, 30}, {43, 28}, {59, 30}, {70, 30}}}, // 37
|
||||||
|
{30, 26, 3362, 0x26a64, [4]level{{22, 30}, {45, 28}, {62, 30}, {74, 30}}}, // 38
|
||||||
|
{24, 28, 3532, 0x27541, [4]level{{24, 30}, {47, 28}, {65, 30}, {77, 30}}}, // 39
|
||||||
|
{28, 28, 3706, 0x28c69, [4]level{{25, 30}, {49, 28}, {68, 30}, {81, 30}}}, // 40
|
||||||
|
}
|
||||||
|
|
||||||
|
func grid(siz int) [][]Pixel {
|
||||||
|
m := make([][]Pixel, siz)
|
||||||
|
pix := make([]Pixel, siz*siz)
|
||||||
|
for i := range m {
|
||||||
|
m[i], pix = pix[:siz], pix[siz:]
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// vplan creates a Plan for the given version.
|
||||||
|
func vplan(v Version) (*Plan, error) {
|
||||||
|
p := &Plan{Version: v}
|
||||||
|
if v < 1 || v > 40 {
|
||||||
|
return nil, fmt.Errorf("invalid QR version %d", int(v))
|
||||||
|
}
|
||||||
|
siz := 17 + int(v)*4
|
||||||
|
m := grid(siz)
|
||||||
|
p.Pixel = m
|
||||||
|
|
||||||
|
// Timing markers (overwritten by boxes).
|
||||||
|
const ti = 6 // timing is in row/column 6 (counting from 0)
|
||||||
|
for i := range m {
|
||||||
|
p := Timing.Pixel()
|
||||||
|
if i&1 == 0 {
|
||||||
|
p |= Black
|
||||||
|
}
|
||||||
|
m[i][ti] = p
|
||||||
|
m[ti][i] = p
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position boxes.
|
||||||
|
posBox(m, 0, 0)
|
||||||
|
posBox(m, siz-7, 0)
|
||||||
|
posBox(m, 0, siz-7)
|
||||||
|
|
||||||
|
// Alignment boxes.
|
||||||
|
info := &vtab[v]
|
||||||
|
for x := 4; x+5 < siz; {
|
||||||
|
for y := 4; y+5 < siz; {
|
||||||
|
// don't overwrite timing markers
|
||||||
|
if (x < 7 && y < 7) || (x < 7 && y+5 >= siz-7) || (x+5 >= siz-7 && y < 7) {
|
||||||
|
} else {
|
||||||
|
alignBox(m, x, y)
|
||||||
|
}
|
||||||
|
if y == 4 {
|
||||||
|
y = info.apos
|
||||||
|
} else {
|
||||||
|
y += info.astride
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if x == 4 {
|
||||||
|
x = info.apos
|
||||||
|
} else {
|
||||||
|
x += info.astride
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version pattern.
|
||||||
|
pat := vtab[v].pattern
|
||||||
|
if pat != 0 {
|
||||||
|
v := pat
|
||||||
|
for x := 0; x < 6; x++ {
|
||||||
|
for y := 0; y < 3; y++ {
|
||||||
|
p := PVersion.Pixel()
|
||||||
|
if v&1 != 0 {
|
||||||
|
p |= Black
|
||||||
|
}
|
||||||
|
m[siz-11+y][x] = p
|
||||||
|
m[x][siz-11+y] = p
|
||||||
|
v >>= 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// One lonely black pixel
|
||||||
|
m[siz-8][8] = Unused.Pixel() | Black
|
||||||
|
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fplan adds the format pixels
|
||||||
|
func fplan(l Level, m Mask, p *Plan) error {
|
||||||
|
// Format pixels.
|
||||||
|
fb := uint32(l^1) << 13 // level: L=01, M=00, Q=11, H=10
|
||||||
|
fb |= uint32(m) << 10 // mask
|
||||||
|
const formatPoly = 0x537
|
||||||
|
rem := fb
|
||||||
|
for i := 14; i >= 10; i-- {
|
||||||
|
if rem&(1<<uint(i)) != 0 {
|
||||||
|
rem ^= formatPoly << uint(i-10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fb |= rem
|
||||||
|
invert := uint32(0x5412)
|
||||||
|
siz := len(p.Pixel)
|
||||||
|
for i := uint(0); i < 15; i++ {
|
||||||
|
pix := Format.Pixel() + OffsetPixel(i)
|
||||||
|
if (fb>>i)&1 == 1 {
|
||||||
|
pix |= Black
|
||||||
|
}
|
||||||
|
if (invert>>i)&1 == 1 {
|
||||||
|
pix ^= Invert | Black
|
||||||
|
}
|
||||||
|
// top left
|
||||||
|
switch {
|
||||||
|
case i < 6:
|
||||||
|
p.Pixel[i][8] = pix
|
||||||
|
case i < 8:
|
||||||
|
p.Pixel[i+1][8] = pix
|
||||||
|
case i < 9:
|
||||||
|
p.Pixel[8][7] = pix
|
||||||
|
default:
|
||||||
|
p.Pixel[8][14-i] = pix
|
||||||
|
}
|
||||||
|
// bottom right
|
||||||
|
switch {
|
||||||
|
case i < 8:
|
||||||
|
p.Pixel[8][siz-1-int(i)] = pix
|
||||||
|
default:
|
||||||
|
p.Pixel[siz-1-int(14-i)][8] = pix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// lplan edits a version-only Plan to add information
|
||||||
|
// about the error correction levels.
|
||||||
|
func lplan(v Version, l Level, p *Plan) error {
|
||||||
|
p.Level = l
|
||||||
|
|
||||||
|
nblock := vtab[v].level[l].nblock
|
||||||
|
ne := vtab[v].level[l].check
|
||||||
|
nde := (vtab[v].bytes - ne*nblock) / nblock
|
||||||
|
extra := (vtab[v].bytes - ne*nblock) % nblock
|
||||||
|
dataBits := (nde*nblock + extra) * 8
|
||||||
|
checkBits := ne * nblock * 8
|
||||||
|
|
||||||
|
p.DataBytes = vtab[v].bytes - ne*nblock
|
||||||
|
p.CheckBytes = ne * nblock
|
||||||
|
p.Blocks = nblock
|
||||||
|
|
||||||
|
// Make data + checksum pixels.
|
||||||
|
data := make([]Pixel, dataBits)
|
||||||
|
for i := range data {
|
||||||
|
data[i] = Data.Pixel() | OffsetPixel(uint(i))
|
||||||
|
}
|
||||||
|
check := make([]Pixel, checkBits)
|
||||||
|
for i := range check {
|
||||||
|
check[i] = Check.Pixel() | OffsetPixel(uint(i+dataBits))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split into blocks.
|
||||||
|
dataList := make([][]Pixel, nblock)
|
||||||
|
checkList := make([][]Pixel, nblock)
|
||||||
|
for i := 0; i < nblock; i++ {
|
||||||
|
// The last few blocks have an extra data byte (8 pixels).
|
||||||
|
nd := nde
|
||||||
|
if i >= nblock-extra {
|
||||||
|
nd++
|
||||||
|
}
|
||||||
|
dataList[i], data = data[0:nd*8], data[nd*8:]
|
||||||
|
checkList[i], check = check[0:ne*8], check[ne*8:]
|
||||||
|
}
|
||||||
|
if len(data) != 0 || len(check) != 0 {
|
||||||
|
panic("data/check math")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build up bit sequence, taking first byte of each block,
|
||||||
|
// then second byte, and so on. Then checksums.
|
||||||
|
bits := make([]Pixel, dataBits+checkBits)
|
||||||
|
dst := bits
|
||||||
|
for i := 0; i < nde+1; i++ {
|
||||||
|
for _, b := range dataList {
|
||||||
|
if i*8 < len(b) {
|
||||||
|
copy(dst, b[i*8:(i+1)*8])
|
||||||
|
dst = dst[8:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := 0; i < ne; i++ {
|
||||||
|
for _, b := range checkList {
|
||||||
|
if i*8 < len(b) {
|
||||||
|
copy(dst, b[i*8:(i+1)*8])
|
||||||
|
dst = dst[8:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(dst) != 0 {
|
||||||
|
panic("dst math")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sweep up pair of columns,
|
||||||
|
// then down, assigning to right then left pixel.
|
||||||
|
// Repeat.
|
||||||
|
// See Figure 2 of http://www.pclviewer.com/rs2/qrtopology.htm
|
||||||
|
siz := len(p.Pixel)
|
||||||
|
rem := make([]Pixel, 7)
|
||||||
|
for i := range rem {
|
||||||
|
rem[i] = Extra.Pixel()
|
||||||
|
}
|
||||||
|
src := append(bits, rem...)
|
||||||
|
for x := siz; x > 0; {
|
||||||
|
for y := siz - 1; y >= 0; y-- {
|
||||||
|
if p.Pixel[y][x-1].Role() == 0 {
|
||||||
|
p.Pixel[y][x-1], src = src[0], src[1:]
|
||||||
|
}
|
||||||
|
if p.Pixel[y][x-2].Role() == 0 {
|
||||||
|
p.Pixel[y][x-2], src = src[0], src[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
x -= 2
|
||||||
|
if x == 7 { // vertical timing strip
|
||||||
|
x--
|
||||||
|
}
|
||||||
|
for y := 0; y < siz; y++ {
|
||||||
|
if p.Pixel[y][x-1].Role() == 0 {
|
||||||
|
p.Pixel[y][x-1], src = src[0], src[1:]
|
||||||
|
}
|
||||||
|
if p.Pixel[y][x-2].Role() == 0 {
|
||||||
|
p.Pixel[y][x-2], src = src[0], src[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
x -= 2
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mplan edits a version+level-only Plan to add the mask.
|
||||||
|
func mplan(m Mask, p *Plan) error {
|
||||||
|
p.Mask = m
|
||||||
|
for y, row := range p.Pixel {
|
||||||
|
for x, pix := range row {
|
||||||
|
if r := pix.Role(); (r == Data || r == Check || r == Extra) && p.Mask.Invert(y, x) {
|
||||||
|
row[x] ^= Black | Invert
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// posBox draws a position (large) box at upper left x, y.
|
||||||
|
func posBox(m [][]Pixel, x, y int) {
|
||||||
|
pos := Position.Pixel()
|
||||||
|
// box
|
||||||
|
for dy := 0; dy < 7; dy++ {
|
||||||
|
for dx := 0; dx < 7; dx++ {
|
||||||
|
p := pos
|
||||||
|
if dx == 0 || dx == 6 || dy == 0 || dy == 6 || 2 <= dx && dx <= 4 && 2 <= dy && dy <= 4 {
|
||||||
|
p |= Black
|
||||||
|
}
|
||||||
|
m[y+dy][x+dx] = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// white border
|
||||||
|
for dy := -1; dy < 8; dy++ {
|
||||||
|
if 0 <= y+dy && y+dy < len(m) {
|
||||||
|
if x > 0 {
|
||||||
|
m[y+dy][x-1] = pos
|
||||||
|
}
|
||||||
|
if x+7 < len(m) {
|
||||||
|
m[y+dy][x+7] = pos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for dx := -1; dx < 8; dx++ {
|
||||||
|
if 0 <= x+dx && x+dx < len(m) {
|
||||||
|
if y > 0 {
|
||||||
|
m[y-1][x+dx] = pos
|
||||||
|
}
|
||||||
|
if y+7 < len(m) {
|
||||||
|
m[y+7][x+dx] = pos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// alignBox draw an alignment (small) box at upper left x, y.
|
||||||
|
func alignBox(m [][]Pixel, x, y int) {
|
||||||
|
// box
|
||||||
|
align := Alignment.Pixel()
|
||||||
|
for dy := 0; dy < 5; dy++ {
|
||||||
|
for dx := 0; dx < 5; dx++ {
|
||||||
|
p := align
|
||||||
|
if dx == 0 || dx == 4 || dy == 0 || dy == 4 || dx == 2 && dy == 2 {
|
||||||
|
p |= Black
|
||||||
|
}
|
||||||
|
m[y+dy][x+dx] = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
241
vendor/rsc.io/qr/gf256/gf256.go
generated
vendored
Normal file
241
vendor/rsc.io/qr/gf256/gf256.go
generated
vendored
Normal file
|
@ -0,0 +1,241 @@
|
||||||
|
// Copyright 2010 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package gf256 implements arithmetic over the Galois Field GF(256).
|
||||||
|
package gf256 // import "rsc.io/qr/gf256"
|
||||||
|
|
||||||
|
import "strconv"
|
||||||
|
|
||||||
|
// A Field represents an instance of GF(256) defined by a specific polynomial.
|
||||||
|
type Field struct {
|
||||||
|
log [256]byte // log[0] is unused
|
||||||
|
exp [510]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewField returns a new field corresponding to the polynomial poly
|
||||||
|
// and generator α. The Reed-Solomon encoding in QR codes uses
|
||||||
|
// polynomial 0x11d with generator 2.
|
||||||
|
//
|
||||||
|
// The choice of generator α only affects the Exp and Log operations.
|
||||||
|
func NewField(poly, α int) *Field {
|
||||||
|
if poly < 0x100 || poly >= 0x200 || reducible(poly) {
|
||||||
|
panic("gf256: invalid polynomial: " + strconv.Itoa(poly))
|
||||||
|
}
|
||||||
|
|
||||||
|
var f Field
|
||||||
|
x := 1
|
||||||
|
for i := 0; i < 255; i++ {
|
||||||
|
if x == 1 && i != 0 {
|
||||||
|
panic("gf256: invalid generator " + strconv.Itoa(α) +
|
||||||
|
" for polynomial " + strconv.Itoa(poly))
|
||||||
|
}
|
||||||
|
f.exp[i] = byte(x)
|
||||||
|
f.exp[i+255] = byte(x)
|
||||||
|
f.log[x] = byte(i)
|
||||||
|
x = mul(x, α, poly)
|
||||||
|
}
|
||||||
|
f.log[0] = 255
|
||||||
|
for i := 0; i < 255; i++ {
|
||||||
|
if f.log[f.exp[i]] != byte(i) {
|
||||||
|
panic("bad log")
|
||||||
|
}
|
||||||
|
if f.log[f.exp[i+255]] != byte(i) {
|
||||||
|
panic("bad log")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := 1; i < 256; i++ {
|
||||||
|
if f.exp[f.log[i]] != byte(i) {
|
||||||
|
panic("bad log")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &f
|
||||||
|
}
|
||||||
|
|
||||||
|
// nbit returns the number of significant in p.
|
||||||
|
func nbit(p int) uint {
|
||||||
|
n := uint(0)
|
||||||
|
for ; p > 0; p >>= 1 {
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// polyDiv divides the polynomial p by q and returns the remainder.
|
||||||
|
func polyDiv(p, q int) int {
|
||||||
|
np := nbit(p)
|
||||||
|
nq := nbit(q)
|
||||||
|
for ; np >= nq; np-- {
|
||||||
|
if p&(1<<(np-1)) != 0 {
|
||||||
|
p ^= q << (np - nq)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// mul returns the product x*y mod poly, a GF(256) multiplication.
|
||||||
|
func mul(x, y, poly int) int {
|
||||||
|
z := 0
|
||||||
|
for x > 0 {
|
||||||
|
if x&1 != 0 {
|
||||||
|
z ^= y
|
||||||
|
}
|
||||||
|
x >>= 1
|
||||||
|
y <<= 1
|
||||||
|
if y&0x100 != 0 {
|
||||||
|
y ^= poly
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return z
|
||||||
|
}
|
||||||
|
|
||||||
|
// reducible reports whether p is reducible.
|
||||||
|
func reducible(p int) bool {
|
||||||
|
// Multiplying n-bit * n-bit produces (2n-1)-bit,
|
||||||
|
// so if p is reducible, one of its factors must be
|
||||||
|
// of np/2+1 bits or fewer.
|
||||||
|
np := nbit(p)
|
||||||
|
for q := 2; q < 1<<(np/2+1); q++ {
|
||||||
|
if polyDiv(p, q) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add returns the sum of x and y in the field.
|
||||||
|
func (f *Field) Add(x, y byte) byte {
|
||||||
|
return x ^ y
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exp returns the base-α exponential of e in the field.
|
||||||
|
// If e < 0, Exp returns 0.
|
||||||
|
func (f *Field) Exp(e int) byte {
|
||||||
|
if e < 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return f.exp[e%255]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log returns the base-α logarithm of x in the field.
|
||||||
|
// If x == 0, Log returns -1.
|
||||||
|
func (f *Field) Log(x byte) int {
|
||||||
|
if x == 0 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return int(f.log[x])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inv returns the multiplicative inverse of x in the field.
|
||||||
|
// If x == 0, Inv returns 0.
|
||||||
|
func (f *Field) Inv(x byte) byte {
|
||||||
|
if x == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return f.exp[255-f.log[x]]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mul returns the product of x and y in the field.
|
||||||
|
func (f *Field) Mul(x, y byte) byte {
|
||||||
|
if x == 0 || y == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return f.exp[int(f.log[x])+int(f.log[y])]
|
||||||
|
}
|
||||||
|
|
||||||
|
// An RSEncoder implements Reed-Solomon encoding
|
||||||
|
// over a given field using a given number of error correction bytes.
|
||||||
|
type RSEncoder struct {
|
||||||
|
f *Field
|
||||||
|
c int
|
||||||
|
gen []byte
|
||||||
|
lgen []byte
|
||||||
|
p []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Field) gen(e int) (gen, lgen []byte) {
|
||||||
|
// p = 1
|
||||||
|
p := make([]byte, e+1)
|
||||||
|
p[e] = 1
|
||||||
|
|
||||||
|
for i := 0; i < e; i++ {
|
||||||
|
// p *= (x + Exp(i))
|
||||||
|
// p[j] = p[j]*Exp(i) + p[j+1].
|
||||||
|
c := f.Exp(i)
|
||||||
|
for j := 0; j < e; j++ {
|
||||||
|
p[j] = f.Mul(p[j], c) ^ p[j+1]
|
||||||
|
}
|
||||||
|
p[e] = f.Mul(p[e], c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// lp = log p.
|
||||||
|
lp := make([]byte, e+1)
|
||||||
|
for i, c := range p {
|
||||||
|
if c == 0 {
|
||||||
|
lp[i] = 255
|
||||||
|
} else {
|
||||||
|
lp[i] = byte(f.Log(c))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, lp
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRSEncoder returns a new Reed-Solomon encoder
|
||||||
|
// over the given field and number of error correction bytes.
|
||||||
|
func NewRSEncoder(f *Field, c int) *RSEncoder {
|
||||||
|
gen, lgen := f.gen(c)
|
||||||
|
return &RSEncoder{f: f, c: c, gen: gen, lgen: lgen}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ECC writes to check the error correcting code bytes
|
||||||
|
// for data using the given Reed-Solomon parameters.
|
||||||
|
func (rs *RSEncoder) ECC(data []byte, check []byte) {
|
||||||
|
if len(check) < rs.c {
|
||||||
|
panic("gf256: invalid check byte length")
|
||||||
|
}
|
||||||
|
if rs.c == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// The check bytes are the remainder after dividing
|
||||||
|
// data padded with c zeros by the generator polynomial.
|
||||||
|
|
||||||
|
// p = data padded with c zeros.
|
||||||
|
var p []byte
|
||||||
|
n := len(data) + rs.c
|
||||||
|
if len(rs.p) >= n {
|
||||||
|
p = rs.p
|
||||||
|
} else {
|
||||||
|
p = make([]byte, n)
|
||||||
|
}
|
||||||
|
copy(p, data)
|
||||||
|
for i := len(data); i < len(p); i++ {
|
||||||
|
p[i] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Divide p by gen, leaving the remainder in p[len(data):].
|
||||||
|
// p[0] is the most significant term in p, and
|
||||||
|
// gen[0] is the most significant term in the generator,
|
||||||
|
// which is always 1.
|
||||||
|
// To avoid repeated work, we store various values as
|
||||||
|
// lv, not v, where lv = log[v].
|
||||||
|
f := rs.f
|
||||||
|
lgen := rs.lgen[1:]
|
||||||
|
for i := 0; i < len(data); i++ {
|
||||||
|
c := p[i]
|
||||||
|
if c == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
q := p[i+1:]
|
||||||
|
exp := f.exp[f.log[c]:]
|
||||||
|
for j, lg := range lgen {
|
||||||
|
if lg != 255 { // lgen uses 255 for log 0
|
||||||
|
q[j] ^= exp[lg]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
copy(check, p[len(data):])
|
||||||
|
rs.p = p
|
||||||
|
}
|
400
vendor/rsc.io/qr/png.go
generated
vendored
Normal file
400
vendor/rsc.io/qr/png.go
generated
vendored
Normal file
|
@ -0,0 +1,400 @@
|
||||||
|
// Copyright 2011 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package qr
|
||||||
|
|
||||||
|
// PNG writer for QR codes.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"hash"
|
||||||
|
"hash/crc32"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PNG returns a PNG image displaying the code.
|
||||||
|
//
|
||||||
|
// PNG uses a custom encoder tailored to QR codes.
|
||||||
|
// Its compressed size is about 2x away from optimal,
|
||||||
|
// but it runs about 20x faster than calling png.Encode
|
||||||
|
// on c.Image().
|
||||||
|
func (c *Code) PNG() []byte {
|
||||||
|
var p pngWriter
|
||||||
|
return p.encode(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
type pngWriter struct {
|
||||||
|
tmp [16]byte
|
||||||
|
wctmp [4]byte
|
||||||
|
buf bytes.Buffer
|
||||||
|
zlib bitWriter
|
||||||
|
crc hash.Hash32
|
||||||
|
}
|
||||||
|
|
||||||
|
var pngHeader = []byte("\x89PNG\r\n\x1a\n")
|
||||||
|
|
||||||
|
func (w *pngWriter) encode(c *Code) []byte {
|
||||||
|
scale := c.Scale
|
||||||
|
siz := c.Size
|
||||||
|
|
||||||
|
w.buf.Reset()
|
||||||
|
|
||||||
|
// Header
|
||||||
|
w.buf.Write(pngHeader)
|
||||||
|
|
||||||
|
// Header block
|
||||||
|
binary.BigEndian.PutUint32(w.tmp[0:4], uint32((siz+8)*scale))
|
||||||
|
binary.BigEndian.PutUint32(w.tmp[4:8], uint32((siz+8)*scale))
|
||||||
|
w.tmp[8] = 1 // 1-bit
|
||||||
|
w.tmp[9] = 0 // gray
|
||||||
|
w.tmp[10] = 0
|
||||||
|
w.tmp[11] = 0
|
||||||
|
w.tmp[12] = 0
|
||||||
|
w.writeChunk("IHDR", w.tmp[:13])
|
||||||
|
|
||||||
|
// Comment
|
||||||
|
w.writeChunk("tEXt", comment)
|
||||||
|
|
||||||
|
// Data
|
||||||
|
w.zlib.writeCode(c)
|
||||||
|
w.writeChunk("IDAT", w.zlib.bytes.Bytes())
|
||||||
|
|
||||||
|
// End
|
||||||
|
w.writeChunk("IEND", nil)
|
||||||
|
|
||||||
|
return w.buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
var comment = []byte("Software\x00QR-PNG http://qr.swtch.com/")
|
||||||
|
|
||||||
|
func (w *pngWriter) writeChunk(name string, data []byte) {
|
||||||
|
if w.crc == nil {
|
||||||
|
w.crc = crc32.NewIEEE()
|
||||||
|
}
|
||||||
|
binary.BigEndian.PutUint32(w.wctmp[0:4], uint32(len(data)))
|
||||||
|
w.buf.Write(w.wctmp[0:4])
|
||||||
|
w.crc.Reset()
|
||||||
|
copy(w.wctmp[0:4], name)
|
||||||
|
w.buf.Write(w.wctmp[0:4])
|
||||||
|
w.crc.Write(w.wctmp[0:4])
|
||||||
|
w.buf.Write(data)
|
||||||
|
w.crc.Write(data)
|
||||||
|
crc := w.crc.Sum32()
|
||||||
|
binary.BigEndian.PutUint32(w.wctmp[0:4], crc)
|
||||||
|
w.buf.Write(w.wctmp[0:4])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bitWriter) writeCode(c *Code) {
|
||||||
|
const ftNone = 0
|
||||||
|
|
||||||
|
b.adler32.Reset()
|
||||||
|
b.bytes.Reset()
|
||||||
|
b.nbit = 0
|
||||||
|
|
||||||
|
scale := c.Scale
|
||||||
|
siz := c.Size
|
||||||
|
|
||||||
|
// zlib header
|
||||||
|
b.tmp[0] = 0x78
|
||||||
|
b.tmp[1] = 0
|
||||||
|
b.tmp[1] += uint8(31 - (uint16(b.tmp[0])<<8+uint16(b.tmp[1]))%31)
|
||||||
|
b.bytes.Write(b.tmp[0:2])
|
||||||
|
|
||||||
|
// Start flate block.
|
||||||
|
b.writeBits(1, 1, false) // final block
|
||||||
|
b.writeBits(1, 2, false) // compressed, fixed Huffman tables
|
||||||
|
|
||||||
|
// White border.
|
||||||
|
// First row.
|
||||||
|
b.byte(ftNone)
|
||||||
|
n := (scale*(siz+8) + 7) / 8
|
||||||
|
b.byte(255)
|
||||||
|
b.repeat(n-1, 1)
|
||||||
|
// 4*scale rows total.
|
||||||
|
b.repeat((4*scale-1)*(1+n), 1+n)
|
||||||
|
|
||||||
|
for i := 0; i < 4*scale; i++ {
|
||||||
|
b.adler32.WriteNByte(ftNone, 1)
|
||||||
|
b.adler32.WriteNByte(255, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
row := make([]byte, 1+n)
|
||||||
|
for y := 0; y < siz; y++ {
|
||||||
|
row[0] = ftNone
|
||||||
|
j := 1
|
||||||
|
var z uint8
|
||||||
|
nz := 0
|
||||||
|
for x := -4; x < siz+4; x++ {
|
||||||
|
// Raw data.
|
||||||
|
for i := 0; i < scale; i++ {
|
||||||
|
z <<= 1
|
||||||
|
if !c.Black(x, y) {
|
||||||
|
z |= 1
|
||||||
|
}
|
||||||
|
if nz++; nz == 8 {
|
||||||
|
row[j] = z
|
||||||
|
j++
|
||||||
|
nz = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if j < len(row) {
|
||||||
|
row[j] = z
|
||||||
|
}
|
||||||
|
for _, z := range row {
|
||||||
|
b.byte(z)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale-1 copies.
|
||||||
|
b.repeat((scale-1)*(1+n), 1+n)
|
||||||
|
|
||||||
|
b.adler32.WriteN(row, scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
// White border.
|
||||||
|
// First row.
|
||||||
|
b.byte(ftNone)
|
||||||
|
b.byte(255)
|
||||||
|
b.repeat(n-1, 1)
|
||||||
|
// 4*scale rows total.
|
||||||
|
b.repeat((4*scale-1)*(1+n), 1+n)
|
||||||
|
|
||||||
|
for i := 0; i < 4*scale; i++ {
|
||||||
|
b.adler32.WriteNByte(ftNone, 1)
|
||||||
|
b.adler32.WriteNByte(255, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// End of block.
|
||||||
|
b.hcode(256)
|
||||||
|
b.flushBits()
|
||||||
|
|
||||||
|
// adler32
|
||||||
|
binary.BigEndian.PutUint32(b.tmp[0:], b.adler32.Sum32())
|
||||||
|
b.bytes.Write(b.tmp[0:4])
|
||||||
|
}
|
||||||
|
|
||||||
|
// A bitWriter is a write buffer for bit-oriented data like deflate.
|
||||||
|
type bitWriter struct {
|
||||||
|
bytes bytes.Buffer
|
||||||
|
bit uint32
|
||||||
|
nbit uint
|
||||||
|
|
||||||
|
tmp [4]byte
|
||||||
|
adler32 adigest
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bitWriter) writeBits(bit uint32, nbit uint, rev bool) {
|
||||||
|
// reverse, for huffman codes
|
||||||
|
if rev {
|
||||||
|
br := uint32(0)
|
||||||
|
for i := uint(0); i < nbit; i++ {
|
||||||
|
br |= ((bit >> i) & 1) << (nbit - 1 - i)
|
||||||
|
}
|
||||||
|
bit = br
|
||||||
|
}
|
||||||
|
b.bit |= bit << b.nbit
|
||||||
|
b.nbit += nbit
|
||||||
|
for b.nbit >= 8 {
|
||||||
|
b.bytes.WriteByte(byte(b.bit))
|
||||||
|
b.bit >>= 8
|
||||||
|
b.nbit -= 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bitWriter) flushBits() {
|
||||||
|
if b.nbit > 0 {
|
||||||
|
b.bytes.WriteByte(byte(b.bit))
|
||||||
|
b.nbit = 0
|
||||||
|
b.bit = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bitWriter) hcode(v int) {
|
||||||
|
/*
|
||||||
|
Lit Value Bits Codes
|
||||||
|
--------- ---- -----
|
||||||
|
0 - 143 8 00110000 through
|
||||||
|
10111111
|
||||||
|
144 - 255 9 110010000 through
|
||||||
|
111111111
|
||||||
|
256 - 279 7 0000000 through
|
||||||
|
0010111
|
||||||
|
280 - 287 8 11000000 through
|
||||||
|
11000111
|
||||||
|
*/
|
||||||
|
switch {
|
||||||
|
case v <= 143:
|
||||||
|
b.writeBits(uint32(v)+0x30, 8, true)
|
||||||
|
case v <= 255:
|
||||||
|
b.writeBits(uint32(v-144)+0x190, 9, true)
|
||||||
|
case v <= 279:
|
||||||
|
b.writeBits(uint32(v-256)+0, 7, true)
|
||||||
|
case v <= 287:
|
||||||
|
b.writeBits(uint32(v-280)+0xc0, 8, true)
|
||||||
|
default:
|
||||||
|
panic("invalid hcode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bitWriter) byte(x byte) {
|
||||||
|
b.hcode(int(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bitWriter) codex(c int, val int, nx uint) {
|
||||||
|
b.hcode(c + val>>nx)
|
||||||
|
b.writeBits(uint32(val)&(1<<nx-1), nx, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bitWriter) repeat(n, d int) {
|
||||||
|
for ; n >= 258+3; n -= 258 {
|
||||||
|
b.repeat1(258, d)
|
||||||
|
}
|
||||||
|
if n > 258 {
|
||||||
|
// 258 < n < 258+3
|
||||||
|
b.repeat1(10, d)
|
||||||
|
b.repeat1(n-10, d)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if n < 3 {
|
||||||
|
panic("invalid flate repeat")
|
||||||
|
}
|
||||||
|
b.repeat1(n, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bitWriter) repeat1(n, d int) {
|
||||||
|
/*
|
||||||
|
Extra Extra Extra
|
||||||
|
Code Bits Length(s) Code Bits Lengths Code Bits Length(s)
|
||||||
|
---- ---- ------ ---- ---- ------- ---- ---- -------
|
||||||
|
257 0 3 267 1 15,16 277 4 67-82
|
||||||
|
258 0 4 268 1 17,18 278 4 83-98
|
||||||
|
259 0 5 269 2 19-22 279 4 99-114
|
||||||
|
260 0 6 270 2 23-26 280 4 115-130
|
||||||
|
261 0 7 271 2 27-30 281 5 131-162
|
||||||
|
262 0 8 272 2 31-34 282 5 163-194
|
||||||
|
263 0 9 273 3 35-42 283 5 195-226
|
||||||
|
264 0 10 274 3 43-50 284 5 227-257
|
||||||
|
265 1 11,12 275 3 51-58 285 0 258
|
||||||
|
266 1 13,14 276 3 59-66
|
||||||
|
*/
|
||||||
|
switch {
|
||||||
|
case n <= 10:
|
||||||
|
b.codex(257, n-3, 0)
|
||||||
|
case n <= 18:
|
||||||
|
b.codex(265, n-11, 1)
|
||||||
|
case n <= 34:
|
||||||
|
b.codex(269, n-19, 2)
|
||||||
|
case n <= 66:
|
||||||
|
b.codex(273, n-35, 3)
|
||||||
|
case n <= 130:
|
||||||
|
b.codex(277, n-67, 4)
|
||||||
|
case n <= 257:
|
||||||
|
b.codex(281, n-131, 5)
|
||||||
|
case n == 258:
|
||||||
|
b.hcode(285)
|
||||||
|
default:
|
||||||
|
panic("invalid repeat length")
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Extra Extra Extra
|
||||||
|
Code Bits Dist Code Bits Dist Code Bits Distance
|
||||||
|
---- ---- ---- ---- ---- ------ ---- ---- --------
|
||||||
|
0 0 1 10 4 33-48 20 9 1025-1536
|
||||||
|
1 0 2 11 4 49-64 21 9 1537-2048
|
||||||
|
2 0 3 12 5 65-96 22 10 2049-3072
|
||||||
|
3 0 4 13 5 97-128 23 10 3073-4096
|
||||||
|
4 1 5,6 14 6 129-192 24 11 4097-6144
|
||||||
|
5 1 7,8 15 6 193-256 25 11 6145-8192
|
||||||
|
6 2 9-12 16 7 257-384 26 12 8193-12288
|
||||||
|
7 2 13-16 17 7 385-512 27 12 12289-16384
|
||||||
|
8 3 17-24 18 8 513-768 28 13 16385-24576
|
||||||
|
9 3 25-32 19 8 769-1024 29 13 24577-32768
|
||||||
|
*/
|
||||||
|
if d <= 4 {
|
||||||
|
b.writeBits(uint32(d-1), 5, true)
|
||||||
|
} else if d <= 32768 {
|
||||||
|
nbit := uint(16)
|
||||||
|
for d <= 1<<(nbit-1) {
|
||||||
|
nbit--
|
||||||
|
}
|
||||||
|
v := uint32(d - 1)
|
||||||
|
v &^= 1 << (nbit - 1) // top bit is implicit
|
||||||
|
code := uint32(2*nbit - 2) // second bit is low bit of code
|
||||||
|
code |= v >> (nbit - 2)
|
||||||
|
v &^= 1 << (nbit - 2)
|
||||||
|
b.writeBits(code, 5, true)
|
||||||
|
// rest of bits follow
|
||||||
|
b.writeBits(uint32(v), nbit-2, false)
|
||||||
|
} else {
|
||||||
|
panic("invalid repeat distance")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bitWriter) run(v byte, n int) {
|
||||||
|
if n == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b.byte(v)
|
||||||
|
if n-1 < 3 {
|
||||||
|
for i := 0; i < n-1; i++ {
|
||||||
|
b.byte(v)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
b.repeat(n-1, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type adigest struct {
|
||||||
|
a, b uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *adigest) Reset() { d.a, d.b = 1, 0 }
|
||||||
|
|
||||||
|
const amod = 65521
|
||||||
|
|
||||||
|
func aupdate(a, b uint32, pi byte, n int) (aa, bb uint32) {
|
||||||
|
// TODO(rsc): 6g doesn't do magic multiplies for b %= amod,
|
||||||
|
// only for b = b%amod.
|
||||||
|
|
||||||
|
// invariant: a, b < amod
|
||||||
|
if pi == 0 {
|
||||||
|
b += uint32(n%amod) * a
|
||||||
|
b = b % amod
|
||||||
|
return a, b
|
||||||
|
}
|
||||||
|
|
||||||
|
// n times:
|
||||||
|
// a += pi
|
||||||
|
// b += a
|
||||||
|
// is same as
|
||||||
|
// b += n*a + n*(n+1)/2*pi
|
||||||
|
// a += n*pi
|
||||||
|
m := uint32(n)
|
||||||
|
b += (m % amod) * a
|
||||||
|
b = b % amod
|
||||||
|
b += (m * (m + 1) / 2) % amod * uint32(pi)
|
||||||
|
b = b % amod
|
||||||
|
a += (m % amod) * uint32(pi)
|
||||||
|
a = a % amod
|
||||||
|
return a, b
|
||||||
|
}
|
||||||
|
|
||||||
|
func afinish(a, b uint32) uint32 {
|
||||||
|
return b<<16 | a
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *adigest) WriteN(p []byte, n int) {
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
for _, pi := range p {
|
||||||
|
d.a, d.b = aupdate(d.a, d.b, pi, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *adigest) WriteNByte(pi byte, n int) {
|
||||||
|
d.a, d.b = aupdate(d.a, d.b, pi, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *adigest) Sum32() uint32 { return afinish(d.a, d.b) }
|
116
vendor/rsc.io/qr/qr.go
generated
vendored
Normal file
116
vendor/rsc.io/qr/qr.go
generated
vendored
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
// Copyright 2011 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
/*
|
||||||
|
Package qr encodes QR codes.
|
||||||
|
*/
|
||||||
|
package qr // import "rsc.io/qr"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"rsc.io/qr/coding"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Level denotes a QR error correction level.
|
||||||
|
// From least to most tolerant of errors, they are L, M, Q, H.
|
||||||
|
type Level int
|
||||||
|
|
||||||
|
const (
|
||||||
|
L Level = iota // 20% redundant
|
||||||
|
M // 38% redundant
|
||||||
|
Q // 55% redundant
|
||||||
|
H // 65% redundant
|
||||||
|
)
|
||||||
|
|
||||||
|
// Encode returns an encoding of text at the given error correction level.
|
||||||
|
func Encode(text string, level Level) (*Code, error) {
|
||||||
|
// Pick data encoding, smallest first.
|
||||||
|
// We could split the string and use different encodings
|
||||||
|
// but that seems like overkill for now.
|
||||||
|
var enc coding.Encoding
|
||||||
|
switch {
|
||||||
|
case coding.Num(text).Check() == nil:
|
||||||
|
enc = coding.Num(text)
|
||||||
|
case coding.Alpha(text).Check() == nil:
|
||||||
|
enc = coding.Alpha(text)
|
||||||
|
default:
|
||||||
|
enc = coding.String(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick size.
|
||||||
|
l := coding.Level(level)
|
||||||
|
var v coding.Version
|
||||||
|
for v = coding.MinVersion; ; v++ {
|
||||||
|
if v > coding.MaxVersion {
|
||||||
|
return nil, errors.New("text too long to encode as QR")
|
||||||
|
}
|
||||||
|
if enc.Bits(v) <= v.DataBytes(l)*8 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build and execute plan.
|
||||||
|
p, err := coding.NewPlan(v, l, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cc, err := p.Encode(enc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Pick appropriate mask.
|
||||||
|
|
||||||
|
return &Code{cc.Bitmap, cc.Size, cc.Stride, 8}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Code is a square pixel grid.
|
||||||
|
// It implements image.Image and direct PNG encoding.
|
||||||
|
type Code struct {
|
||||||
|
Bitmap []byte // 1 is black, 0 is white
|
||||||
|
Size int // number of pixels on a side
|
||||||
|
Stride int // number of bytes per row
|
||||||
|
Scale int // number of image pixels per QR pixel
|
||||||
|
}
|
||||||
|
|
||||||
|
// Black returns true if the pixel at (x,y) is black.
|
||||||
|
func (c *Code) Black(x, y int) bool {
|
||||||
|
return 0 <= x && x < c.Size && 0 <= y && y < c.Size &&
|
||||||
|
c.Bitmap[y*c.Stride+x/8]&(1<<uint(7-x&7)) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Image returns an Image displaying the code.
|
||||||
|
func (c *Code) Image() image.Image {
|
||||||
|
return &codeImage{c}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// codeImage implements image.Image
|
||||||
|
type codeImage struct {
|
||||||
|
*Code
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
whiteColor color.Color = color.Gray{0xFF}
|
||||||
|
blackColor color.Color = color.Gray{0x00}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *codeImage) Bounds() image.Rectangle {
|
||||||
|
d := (c.Size + 8) * c.Scale
|
||||||
|
return image.Rect(0, 0, d, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *codeImage) At(x, y int) color.Color {
|
||||||
|
if c.Black(x, y) {
|
||||||
|
return blackColor
|
||||||
|
}
|
||||||
|
return whiteColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *codeImage) ColorModel() color.Model {
|
||||||
|
return color.GrayModel
|
||||||
|
}
|
|
@ -220,7 +220,7 @@ func Handle(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.URL.Path {
|
switch r.URL.Path {
|
||||||
case "/":
|
case "/":
|
||||||
if r.Method != "GET" {
|
if r.Method != "GET" {
|
||||||
http.Error(w, "405 - method not allowed - post required", http.StatusMethodNotAllowed)
|
http.Error(w, "405 - method not allowed - get required", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
|
|
@ -670,16 +670,25 @@ const destination = async (name) => {
|
||||||
}
|
}
|
||||||
page.classList.add('loading')
|
page.classList.add('loading')
|
||||||
await api.DestinationSave(name, dest, newDest)
|
await api.DestinationSave(name, dest, newDest)
|
||||||
dest = newDest // Set new dest, for if user edits again. Without this, they would get an error that the config has been modified.
|
window.location.reload() // todo: only refresh part of ui
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log({err})
|
console.log({err})
|
||||||
window.alert('Error: '+err.message)
|
window.alert('Error: '+err.message)
|
||||||
|
page.classList.remove('loading')
|
||||||
return
|
return
|
||||||
} finally {
|
} finally {
|
||||||
saveButton.disabled = false
|
saveButton.disabled = false
|
||||||
page.classList.remove('loading')
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
dom.br(),
|
||||||
|
dom.br(),
|
||||||
|
dom.br(),
|
||||||
|
dom.p("Apple's mail applications don't do account autoconfiguration, and when adding an account it can choose defaults that don't work with modern email servers. Adding an account through a \"mobileconfig\" profile file can be more convenient: It contains the IMAP/SMTP settings such as host name, port, TLS, authentication mechanism and user name. This profile does not contain a login password. Opening the profile adds it under Profiles in System Preferences (macOS) or Settings (iOS), where you can install it. These profiles are not signed, so users will have to ignore the warnings about them being unsigned. ",
|
||||||
|
dom.br(),
|
||||||
|
dom.a(attr({href: 'https://autoconfig.'+domainName(domain)+'/profile.mobileconfig?address='+encodeURIComponent(name)+'&name='+encodeURIComponent(dest.FullName), download: ''}), 'Download .mobileconfig email account profile'),
|
||||||
|
dom.br(),
|
||||||
|
dom.a(attr({href: 'https://autoconfig.'+domainName(domain)+'/profile.mobileconfig.qrcode.png?address='+encodeURIComponent(name)+'&name='+encodeURIComponent(dest.FullName), download: ''}), 'Open QR-code with link to .mobileconfig profile'),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1578,7 +1578,8 @@ func (Admin) DomainRemove(ctx context.Context, domain string) {
|
||||||
xcheckf(ctx, err, "removing domain")
|
xcheckf(ctx, err, "removing domain")
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountAdd adds existing a new account, with an initial email address, and reloads the configuration.
|
// AccountAdd adds existing a new account, with an initial email address, and
|
||||||
|
// reloads the configuration.
|
||||||
func (Admin) AccountAdd(ctx context.Context, accountName, address string) {
|
func (Admin) AccountAdd(ctx context.Context, accountName, address string) {
|
||||||
err := mox.AccountAdd(ctx, accountName, address)
|
err := mox.AccountAdd(ctx, accountName, address)
|
||||||
xcheckf(ctx, err, "adding account")
|
xcheckf(ctx, err, "adding account")
|
||||||
|
@ -1625,13 +1626,13 @@ func (Admin) SetAccountLimits(ctx context.Context, accountName string, maxOutgoi
|
||||||
xcheckf(ctx, err, "saving account limits")
|
xcheckf(ctx, err, "saving account limits")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClientConfigDomain returns configurations for email clients, IMAP and
|
// ClientConfigsDomain returns configurations for email clients, IMAP and
|
||||||
// Submission (SMTP) for the domain.
|
// Submission (SMTP) for the domain.
|
||||||
func (Admin) ClientConfigDomain(ctx context.Context, domain string) mox.ClientConfig {
|
func (Admin) ClientConfigsDomain(ctx context.Context, domain string) mox.ClientConfigs {
|
||||||
d, err := dns.ParseDomain(domain)
|
d, err := dns.ParseDomain(domain)
|
||||||
xcheckuserf(ctx, err, "parsing domain")
|
xcheckuserf(ctx, err, "parsing domain")
|
||||||
|
|
||||||
cc, err := mox.ClientConfigDomain(d)
|
cc, err := mox.ClientConfigsDomain(d)
|
||||||
xcheckf(ctx, err, "client config for domain")
|
xcheckf(ctx, err, "client config for domain")
|
||||||
return cc
|
return cc
|
||||||
}
|
}
|
||||||
|
|
|
@ -695,12 +695,12 @@ const account = async (name) => {
|
||||||
const domain = async (d) => {
|
const domain = async (d) => {
|
||||||
const end = new Date().toISOString()
|
const end = new Date().toISOString()
|
||||||
const start = new Date(new Date().getTime() - 30*24*3600*1000).toISOString()
|
const start = new Date(new Date().getTime() - 30*24*3600*1000).toISOString()
|
||||||
const [dmarcSummaries, tlsrptSummaries, localpartAccounts, dnsdomain, clientConfig] = await Promise.all([
|
const [dmarcSummaries, tlsrptSummaries, localpartAccounts, dnsdomain, clientConfigs] = await Promise.all([
|
||||||
api.DMARCSummaries(start, end, d),
|
api.DMARCSummaries(start, end, d),
|
||||||
api.TLSRPTSummaries(start, end, d),
|
api.TLSRPTSummaries(start, end, d),
|
||||||
api.DomainLocalparts(d),
|
api.DomainLocalparts(d),
|
||||||
api.Domain(d),
|
api.Domain(d),
|
||||||
api.ClientConfigDomain(d),
|
api.ClientConfigsDomain(d),
|
||||||
])
|
])
|
||||||
|
|
||||||
let form, fieldset, localpart, account
|
let form, fieldset, localpart, account
|
||||||
|
@ -725,7 +725,7 @@ const domain = async (d) => {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
dom.tbody(
|
dom.tbody(
|
||||||
clientConfig.Entries.map(e =>
|
clientConfigs.Entries.map(e =>
|
||||||
dom.tr(
|
dom.tr(
|
||||||
dom.td(e.Protocol),
|
dom.td(e.Protocol),
|
||||||
dom.td(domainString(e.Host)),
|
dom.td(domainString(e.Host)),
|
||||||
|
@ -756,7 +756,7 @@ const domain = async (d) => {
|
||||||
dom.td(t[0] || '(catchall)'),
|
dom.td(t[0] || '(catchall)'),
|
||||||
dom.td(dom.a(t[1], attr({href: '#accounts/'+t[1]}))),
|
dom.td(dom.a(t[1], attr({href: '#accounts/'+t[1]}))),
|
||||||
dom.td(
|
dom.td(
|
||||||
dom.button('Remove address', async function click(e) {
|
dom.button('Remove', async function click(e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!window.confirm('Are you sure you want to remove this address?')) {
|
if (!window.confirm('Are you sure you want to remove this address?')) {
|
||||||
return
|
return
|
||||||
|
|
|
@ -437,7 +437,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Name": "AccountAdd",
|
"Name": "AccountAdd",
|
||||||
"Docs": "AccountAdd adds existing a new account, with an initial email address, and reloads the configuration.",
|
"Docs": "AccountAdd adds existing a new account, with an initial email address, and\nreloads the configuration.",
|
||||||
"Params": [
|
"Params": [
|
||||||
{
|
{
|
||||||
"Name": "accountName",
|
"Name": "accountName",
|
||||||
|
@ -544,8 +544,8 @@
|
||||||
"Returns": []
|
"Returns": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Name": "ClientConfigDomain",
|
"Name": "ClientConfigsDomain",
|
||||||
"Docs": "ClientConfigDomain returns configurations for email clients, IMAP and\nSubmission (SMTP) for the domain.",
|
"Docs": "ClientConfigsDomain returns configurations for email clients, IMAP and\nSubmission (SMTP) for the domain.",
|
||||||
"Params": [
|
"Params": [
|
||||||
{
|
{
|
||||||
"Name": "domain",
|
"Name": "domain",
|
||||||
|
@ -558,7 +558,7 @@
|
||||||
{
|
{
|
||||||
"Name": "r0",
|
"Name": "r0",
|
||||||
"Typewords": [
|
"Typewords": [
|
||||||
"ClientConfig"
|
"ClientConfigs"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -2772,21 +2772,21 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Name": "ClientConfig",
|
"Name": "ClientConfigs",
|
||||||
"Docs": "ClientConfig holds the client configuration for IMAP/Submission for a\ndomain.",
|
"Docs": "ClientConfigs holds the client configuration for IMAP/Submission for a\ndomain.",
|
||||||
"Fields": [
|
"Fields": [
|
||||||
{
|
{
|
||||||
"Name": "Entries",
|
"Name": "Entries",
|
||||||
"Docs": "",
|
"Docs": "",
|
||||||
"Typewords": [
|
"Typewords": [
|
||||||
"[]",
|
"[]",
|
||||||
"ClientConfigEntry"
|
"ClientConfigsEntry"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Name": "ClientConfigEntry",
|
"Name": "ClientConfigsEntry",
|
||||||
"Docs": "",
|
"Docs": "",
|
||||||
"Fields": [
|
"Fields": [
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in a new issue