├── README.md
├── cmd
├── spillbox
│ └── spillbox_main.go
└── spilld
│ └── main.go
├── email
├── address.go
├── dkim
│ ├── dkim_sign.go
│ ├── dkim_sign_test.go
│ ├── dkim_verify.go
│ ├── dkim_verify_test.go
│ └── verifier.go
├── header.go
├── header_test.go
├── message.go
├── msgbuilder
│ ├── msgbuilder.go
│ ├── msgbuilder_test.go
│ └── tree.go
└── msgcleaver
│ ├── msgcleaver.go
│ └── msgcleaver_test.go
├── html
├── css
│ ├── doc.go
│ ├── format.go
│ ├── format_test.go
│ ├── parser.go
│ ├── parser_test.go
│ ├── scanner.go
│ ├── scanner_test.go
│ ├── sourcereader.go
│ ├── token_string.go
│ ├── typeflag_string.go
│ └── valuetype_string.go
├── htmlembed
│ ├── htmlembed.go
│ └── htmlembed_test.go
└── htmlsafe
│ ├── htmlsafe.go
│ └── htmlsafe_test.go
├── imap
├── imap.go
├── imapparser
│ ├── parser.go
│ ├── parser_test.go
│ ├── scanner.go
│ ├── scanner_test.go
│ ├── search.go
│ ├── search_test.go
│ ├── types.go
│ ├── typeshelp.go
│ └── utf7mod
│ │ ├── utf7mod.go
│ │ └── utf7mod_test.go
├── imapserver
│ ├── apns.go
│ ├── debug.go
│ ├── fetch.go
│ ├── imapserver.go
│ ├── imapserver_test.go
│ ├── log.go
│ └── toyserver.go
└── imaptest
│ ├── commandtest.go
│ ├── condstoretest.go
│ ├── fetchtest.go
│ ├── memory.go
│ └── servertest.go
├── smtp
├── smtpclient
│ └── smtpclient.go
└── smtpserver
│ ├── greylist
│ └── greylist.go
│ ├── smtpserver.go
│ └── smtpserver_test.go
├── spilldb
├── boxmgmt
│ └── boxmgmt.go
├── db
│ ├── auth.go
│ ├── auth_test.go
│ ├── db.go
│ ├── db_test.go
│ ├── janitor.go
│ └── sql.go
├── deliverer
│ └── deliverer.go
├── dnsdb
│ └── dnsdb.go
├── greylistdb
│ └── greylistdb.go
├── honeypotdb
│ └── honeypotdb.go
├── imapdb
│ ├── imapdb.go
│ └── imapdb_test.go
├── localsender
│ └── localsender.go
├── processor
│ └── processor.go
├── smtpdb
│ └── smtpdb.go
├── spillbox
│ ├── contact.go
│ ├── insertmsg.go
│ ├── mailbox.go
│ ├── normalize.go
│ ├── prettyhtml
│ │ ├── prettyhtml.go
│ │ └── prettyhtml_test.go
│ ├── printmsg.go
│ ├── spillbox.go
│ └── sql_spillbox.go
├── spilldb.go
└── webcache
│ ├── webcache.go
│ └── webcache_test.go
├── testdata
├── msg1.eml
├── msg2.eml
├── msg3.eml
├── msg4.eml
└── msg5.eml
├── third_party
├── dns
│ ├── AUTHORS
│ ├── CONTRIBUTORS
│ ├── COPYRIGHT
│ ├── LICENSE
│ ├── README
│ ├── acceptfunc.go
│ ├── client.go
│ ├── defaults.go
│ ├── dns.go
│ ├── dns_bench_test.go
│ ├── dns_test.go
│ ├── duplicate.go
│ ├── duplicate_generate.go
│ ├── duplicate_test.go
│ ├── dyn_test.go
│ ├── generate.go
│ ├── generate_test.go
│ ├── issue_test.go
│ ├── labels.go
│ ├── labels_test.go
│ ├── leak_test.go
│ ├── length_test.go
│ ├── listen_go111.go
│ ├── listen_go_not111.go
│ ├── msg.go
│ ├── msg_generate.go
│ ├── msg_helpers.go
│ ├── msg_helpers_test.go
│ ├── msg_test.go
│ ├── parse_test.go
│ ├── reverse.go
│ ├── rr_test.go
│ ├── scan.go
│ ├── scan_rr.go
│ ├── scan_test.go
│ ├── serve_mux.go
│ ├── serve_mux_test.go
│ ├── server.go
│ ├── server_test.go
│ ├── types.go
│ ├── types_generate.go
│ ├── types_test.go
│ ├── udp.go
│ ├── udp_test.go
│ ├── update_test.go
│ ├── zduplicate.go
│ ├── zmsg.go
│ └── ztypes.go
└── imf
│ ├── addr.go
│ ├── addr_test.go
│ ├── multipart.go
│ ├── multipart_test.go
│ ├── reader.go
│ ├── reader_test.go
│ ├── testdata
│ └── nested-mime
│ └── textproto.go
└── util
├── devcert
└── devcert.go
├── throttle
├── throttle.go
└── throttle_test.go
└── tlstest
└── tlstest.go
/README.md:
--------------------------------------------------------------------------------
1 | # spilled.ink
2 |
3 | [Spilled.ink](spilled.ink) is a new take on open source email infrastructure.
4 |
5 | This repository contains the spilld SMTP and IMAP server,
6 | built around a new SQLite-based storage format, spillbox.
7 |
8 | The project is very new.
9 |
10 | ## The spilld server
11 |
12 | The main binary is called **spilld**. The development version can be
13 | installed with go get:
14 |
15 | ```go get -u spilled.ink/cmd/spilld```
16 |
17 | ## The spillbox storage format
18 |
19 | **NOTE: this is a pre-release**, and the format is in flux.
20 | It may change in incompatible ways and no tool will be published
21 | to automatically migrate spillbox data until the project uses
22 | version numbers. Store precious data in spillbox at your own risk.
23 |
24 | A lot of documentation will be coming in the next few weeks.
25 |
26 | ## The spillbox tool
27 |
28 | `spillbox` is a command-line tool for managing a spilldb database.
29 | The development version can be installed with go get:
30 |
31 | ```go get -u spilled.ink/cmd/spillbox```
32 |
33 | TODO: document
34 |
35 | ## License
36 |
37 | The code is licensed under the GPL.
38 | If the GPL does not suit your needs, alternate licenses are for sale
39 | at reasonable rates. The goal is not to make big $$$ out of licensing
40 | (ha!), but to make sure the project maintains focus on indie email
41 | servers, while not locking out anyone else who has a reasonable use
42 | for the project.
43 |
44 | To that end, contributors need to sign a contributor license agreement.
--------------------------------------------------------------------------------
/email/address.go:
--------------------------------------------------------------------------------
1 | package email
2 |
3 | // Address is an email address.
4 | type Address struct {
5 | Name string // proper name, may be empty
6 | Addr string // user@domain
7 | }
8 |
--------------------------------------------------------------------------------
/email/dkim/dkim_sign.go:
--------------------------------------------------------------------------------
1 | // Package dkim implements DKIM message signing and verification.
2 | package dkim
3 |
4 | import (
5 | "bytes"
6 | "crypto"
7 | "crypto/rand"
8 | "crypto/rsa"
9 | "crypto/sha256"
10 | "crypto/x509"
11 | "encoding/base64"
12 | "encoding/pem"
13 | "errors"
14 | "fmt"
15 | "io"
16 | "sort"
17 | "strings"
18 | )
19 |
20 | // A Signer signs email with a DKIM-Signature.
21 | type Signer struct {
22 | key *rsa.PrivateKey
23 |
24 | Domain string // d=, signing domain
25 | Selector string // s=, key selector, TXT record is: ._domainkey.
26 | Headers []string // h=, list of headers in lower-case to sign
27 | }
28 |
29 | // NewSigner creates a Signer around a privateKey with prepopulated Headers.
30 | // Set the Domain and Selector fields before using it.
31 | func NewSigner(privateKey []byte) (*Signer, error) {
32 | headers := []string{
33 | "content-type",
34 | "date",
35 | "from",
36 | "in-reply-to",
37 | "message-id",
38 | "mime-version",
39 | "references",
40 | "subject",
41 | "to",
42 | }
43 | sort.Strings(headers)
44 |
45 | block, _ := pem.Decode(privateKey)
46 | if block == nil {
47 | return nil, errors.New("dkim: cannot decode key")
48 | }
49 | key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
50 | if err != nil {
51 | return nil, fmt.Errorf("dkim: cannot parse key: %v", err)
52 | }
53 |
54 | return &Signer{
55 | Headers: headers,
56 | key: key,
57 | }, nil
58 | }
59 |
60 | // Sign signs an email, reporting a new DKIM-Signature header.
61 | // It is safe for use by multiple goroutines simultaneously.
62 | func (s *Signer) Sign(hdr Header, body io.Reader) (dkimHeaderValue []byte, err error) {
63 | h := sha256.New()
64 |
65 | buf := bytes.NewBuffer(make([]byte, 0, 512))
66 | buf.WriteString("v=1; a=rsa-sha256; c=relaxed/relaxed; d=")
67 | buf.WriteString(s.Domain)
68 | buf.WriteString("; s=")
69 | buf.WriteString(s.Selector)
70 | buf.WriteString("; h=")
71 | if err := collectRelaxedHeaders(buf, h, s.Headers, hdr); err != nil {
72 | return nil, err
73 | }
74 | buf.WriteString("; bh=")
75 | if err := relaxedBodyHash(buf, body); err != nil {
76 | return nil, err
77 | }
78 | buf.WriteString("; b=")
79 |
80 | io.WriteString(h, "dkim-signature:")
81 | h.Write(buf.Bytes())
82 |
83 | sig, err := rsa.SignPKCS1v15(rand.Reader, s.key, crypto.SHA256, h.Sum(nil))
84 | if err != nil {
85 | return nil, fmt.Errorf("dkim: %v", err)
86 | }
87 | sigFinal := make([]byte, base64.StdEncoding.EncodedLen(len(sig)))
88 | base64.StdEncoding.Encode(sigFinal, sig)
89 |
90 | // Add folding white space.
91 | // Valid as per RFC 4871, 3.5:
92 | // """
93 | // b= The signature data (base64; REQUIRED). Whitespace is ignored in
94 | // this value and MUST be ignored when reassembling the original
95 | // signature. In particular, the signing process can safely insert
96 | // FWS in this value in arbitrary places to conform to line-length
97 | // limits.
98 | // """
99 | for len(sigFinal) > 0 {
100 | n := len(sigFinal)
101 | if n > 66 {
102 | n = 66
103 | }
104 | buf.Write(sigFinal[:n])
105 | sigFinal = sigFinal[n:]
106 | if len(sigFinal) > 0 {
107 | buf.WriteByte(' ')
108 | }
109 | }
110 | return buf.Bytes(), nil
111 | }
112 |
113 | // Header is the set of MIME headers on the email being signed.
114 | //
115 | // The Get method is called by the signer with lower-case headers
116 | // and it is the responsibility of the implementation to search
117 | // its header names case-insensitively.
118 | type Header interface {
119 | Get(header string) (value string)
120 | }
121 |
122 | func relaxedBodyHash(dst *bytes.Buffer, body io.Reader) error {
123 | var b [sha256.BlockSize]byte
124 | h := sha256.New()
125 | if _, err := io.Copy(h, newRelaxedBody(body)); err != nil {
126 | return fmt.Errorf("dkim: hashing body: %v", err)
127 | }
128 | w := base64.NewEncoder(base64.StdEncoding, dst)
129 | if _, err := w.Write(h.Sum(b[:0])); err != nil {
130 | return err
131 | }
132 | return w.Close()
133 | }
134 |
135 | func collectRelaxedHeaders(dstHeaderKeys *bytes.Buffer, dstHeaderBytes io.Writer, potentialHeaders []string, hdr Header) (err error) {
136 | oneByte := make([]byte, 1)
137 | numHeaders := 0
138 | for _, hdrKey := range potentialHeaders {
139 | v := hdr.Get(hdrKey)
140 | if v == "" {
141 | continue
142 | }
143 | if numHeaders > 0 {
144 | dstHeaderKeys.WriteByte(':')
145 | }
146 | numHeaders++
147 | dstHeaderKeys.WriteString(hdrKey)
148 |
149 | // RFC 6376
150 | // 3.4.2.1:
151 | // Convert all header field names (not the header field values) to
152 | // lowercase. For example, convert "SUBJect: AbC" to "subject: AbC".
153 | if _, err := io.WriteString(dstHeaderBytes, hdrKey); err != nil {
154 | return err
155 | }
156 | // 3.4.2.2:
157 | // Header continuations are already unfolded in email.Header.
158 | //
159 | // 3.4.2.5:
160 | // Delete any WSP characters remaining before and after the colon
161 | // separating the header field name from the header field value. The
162 | // colon separator MUST be retained.
163 | oneByte[0] = ':'
164 | if _, err := dstHeaderBytes.Write(oneByte); err != nil {
165 | return err
166 | }
167 | // 3.4.2.4:
168 | // Delete all WSP characters at the end of each unfolded header field
169 | // value.
170 | v = strings.TrimSpace(v)
171 | // 3.4.2.3:
172 | // Convert all sequences of one or more WSP characters to a single SP
173 | // character. WSP characters here include those before and after a
174 | // line folding boundary.
175 | inWhitespace := false
176 | for i := 0; i < len(v); i++ {
177 | c := v[i]
178 | switch c {
179 | case ' ', '\t':
180 | if inWhitespace {
181 | continue
182 | }
183 | inWhitespace = true
184 | c = ' '
185 | default:
186 | inWhitespace = false
187 | }
188 |
189 | oneByte[0] = c
190 | if _, err := dstHeaderBytes.Write(oneByte); err != nil {
191 | return err
192 | }
193 | }
194 | if _, err := dstHeaderBytes.Write(crlf); err != nil {
195 | return err
196 | }
197 | }
198 | return nil
199 | }
200 |
--------------------------------------------------------------------------------
/email/dkim/dkim_sign_test.go:
--------------------------------------------------------------------------------
1 | package dkim
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "net/mail"
7 | "strings"
8 | "testing"
9 | )
10 |
11 | func TestRelaxedHeaders(t *testing.T) {
12 | potentialHeaders := []string{"a", "b", "c"}
13 |
14 | // From RFC 6376, 3.4.5.
15 | const msg = "A: X \r\n" +
16 | "B : Y \t\r\n" +
17 | "\tZ \r\n" +
18 | "\r\n"
19 |
20 | mmsg, err := mail.ReadMessage(strings.NewReader(msg))
21 | if err != nil {
22 | t.Fatal(err)
23 | }
24 |
25 | headerKeysBuf, out := new(bytes.Buffer), new(bytes.Buffer)
26 | if err := collectRelaxedHeaders(headerKeysBuf, out, potentialHeaders, mmsg.Header); err != nil {
27 | t.Fatal(err)
28 | }
29 | headerKeys := headerKeysBuf.String()
30 |
31 | if want := "a:b"; headerKeys != want {
32 | t.Errorf("headerKeys=%q, want %q", headerKeys, want)
33 | }
34 |
35 | want := "a:X\r\n" +
36 | "b:Y Z\r\n"
37 | if got := out.String(); got != want {
38 | t.Errorf("out=%q, want %q", got, want)
39 | }
40 | }
41 |
42 | var bodyTests = []struct {
43 | body string
44 | hash string
45 | }{
46 | {
47 | body: strings.Replace(`--ff7c7911124c59ff202320f18a3b36be2517cf6b041f6691a6204a69d056
48 | Content-Type: text/html
49 |
50 | Here is some HTML to convert to plain text version.Next line.
Next paragraph.
This is bold, italic, and underlined text.
Regards.
51 | --ff7c7911124c59ff202320f18a3b36be2517cf6b041f6691a6204a69d056
52 | Content-Type: text/plain
53 |
54 | Here is some HTML to convert to plain text version.
55 | Next line.
56 |
57 | Next paragraph.
58 |
59 | This is bold, italic, and underlined text.
60 |
61 | Regards.
62 | --ff7c7911124c59ff202320f18a3b36be2517cf6b041f6691a6204a69d056--
63 | `, "\n", "\r\n", -1),
64 | hash: "oYXqSYgyGrxRT93p/bOPMxrm2ZTGd3fnMMcXhjwuPkg=", // produced by ARC-Message-Signature c=relaxed/relaxed on gmail
65 | },
66 | }
67 |
68 | func TestBodies(t *testing.T) {
69 | for i, bt := range bodyTests {
70 | bt := bt
71 | t.Run(fmt.Sprintf("i=%d", i), func(t *testing.T) {
72 | buf := new(bytes.Buffer)
73 | err := relaxedBodyHash(buf, strings.NewReader(bt.body))
74 | if err != nil {
75 | t.Fatal(err)
76 | }
77 | got := buf.String()
78 | if got != bt.hash {
79 | t.Errorf("hash=%s, want %s", got, bt.hash)
80 | }
81 | })
82 | }
83 | }
84 |
85 | var testPrivateKey = `-----BEGIN RSA PRIVATE KEY-----
86 | MIICXQIBAAKBgQDlPKmFqjWCqh4kZqdAoQmOWD695FTqiuGNEXtADNOt2PlmRjbi
87 | LOwPJWdzTAjbABPddmPHJXDPLolEDPKbeOAdsBogvpw6ZKvGNd5ZcXYNyX7j2oyG
88 | +RO5TbBSYWLfB1QgJWXztfUrPxWkd50CD6Ht11KA6h31coW2JYcbtRMbpwIDAQAB
89 | AoGBAL5bz5I1s9XbmsgzjnP2xk60LPXXZESYK5DPkX+wpx9YbFJnwC+1ihlRwERY
90 | QYpK2DQxmc3H45PIWyhtcBF3IPMz54lMa//IuzsmGz1XgelzEFJY9FbeedCUZvT1
91 | PvOv+fMDg7otT8ueBkfAg2jG+G2ZOm0WQHdMV5iiWY8uFjrRAkEA9b2uf/IW6y/c
92 | HPslOUY4nXOTTG0gfoMmtxuy3ZC3FXemLmXfS+4ueSiPasn8PYz8hnEKfs6mr6kq
93 | 9tJCB7A+8wJBAO7OmMetEEAqfTZtOxMJz4XOfrbKP+vOHVEkgIYuyEyQqZS/3zKm
94 | 9LrtvejrBpmGXyo2wO+6m4kmG/1yCYS35X0CQAJ1s5l0QuZ3xCxGF0lLeqWY0pCh
95 | RwH9LhYHIPM2z55XZEJyopmP+McdsNHQ08WJ870kxIYga2q2tsdhs2eATCECQQDq
96 | 3UeHQl80LFWfXMh3zfNKjy8yiTFasglFT5gT4BjgrHoMMLTMdUVGPyHC3LtN7MjV
97 | lKomXCoyNcfbePeBjvdlAkB2v5ZdS2oIYGrQ2I0pyPXRiXOVWlFreWh+v69mUcDq
98 | pSFcE/MM8J5jjad3nN3cUaVjlbM36/3lKLRwVK024R2C
99 | -----END RSA PRIVATE KEY-----
100 | `
101 |
102 | func TestSigner(t *testing.T) {
103 | s, err := NewSigner([]byte(testPrivateKey))
104 | if err != nil {
105 | t.Fatal(err)
106 | }
107 | s.Domain = "spilled.ink"
108 | s.Selector = "20180812"
109 |
110 | // From RFC 6376, 3.4.5.
111 | const msg = "From: David Crawshaw \r\n" +
112 | "To: sales@thepencilcompany.com\r\n" +
113 | "\r\n" +
114 | "Hello I would like to buy some pencils please.\r\n"
115 | mmsg, err := mail.ReadMessage(strings.NewReader(msg))
116 | if err != nil {
117 | t.Fatal(err)
118 | }
119 | sig, err := s.Sign(mmsg.Header, mmsg.Body)
120 | if err != nil {
121 | t.Fatal(err)
122 | }
123 | got := string(sig)
124 | const want = "v=1; a=rsa-sha256; c=relaxed/relaxed; d=spilled.ink; " +
125 | "s=20180812; h=from:to; " +
126 | "bh=9NQdhsl2Ev6IxT84434gWZr4UlAnR+3pSUMBVeSDexo=; " +
127 | "b=K3Dr9z/GEQdiuNsp5/bwiq3lSoX1G/UGiiV4qpe13GYfwkPnhq5fLZGbgc+B12Y0e9 " +
128 | "H+5E6FlDDx1CAgT0vZovuvoyV/Cc+iiAEzoEO8JTeDBqIh5NcFVEd9z6DVYiYaZvGt " +
129 | "/BZD0zSVIJZtlt8XihiK6Q6o3YXOS/qx7r/GMPk="
130 | t.Log("len(want): ", len(want))
131 |
132 | if got != want {
133 | t.Errorf("signed header:\n%s\n\nwant:\n%s", got, want)
134 | }
135 | }
136 |
137 | func BenchmarkSigner(b *testing.B) {
138 | b.StopTimer()
139 | s, err := NewSigner([]byte(testPrivateKey))
140 | if err != nil {
141 | b.Fatal(err)
142 | }
143 | s.Domain = "spilled.ink"
144 | s.Selector = "20180812"
145 |
146 | const msgHdr = "From: David Crawshaw \r\n" +
147 | "To: sales@thepencilcompany.com\r\n" +
148 | "\r\n"
149 | const msgBody = "Hello I would like to buy some pencils please.\r\n"
150 | mmsg, err := mail.ReadMessage(strings.NewReader(msgHdr))
151 | if err != nil {
152 | b.Fatal(err)
153 | }
154 | hdr := mmsg.Header
155 |
156 | b.ReportAllocs()
157 | b.StartTimer()
158 | b.ResetTimer()
159 | for i := 0; i < b.N; i++ {
160 | if _, err := s.Sign(hdr, strings.NewReader(msgBody)); err != nil {
161 | b.Fatal(err)
162 | }
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/email/dkim/verifier.go:
--------------------------------------------------------------------------------
1 | //+build ignore
2 |
3 | package main
4 |
5 | import (
6 | "bytes"
7 | "context"
8 | "fmt"
9 | "io/ioutil"
10 | "os"
11 |
12 | "spilled.ink/email/dkim"
13 | )
14 |
15 | func main() {
16 | src := os.Stdin
17 | name := "stdin"
18 | if len(os.Args) == 2 {
19 | f, err := os.Open(os.Args[1])
20 | if err != nil {
21 | fmt.Fprintf(os.Stderr, "%s\n", err)
22 | os.Exit(1)
23 | }
24 | src = f
25 | name = os.Args[1]
26 | }
27 |
28 | email, err := ioutil.ReadAll(src)
29 | if err != nil {
30 | fmt.Fprintf(os.Stderr, "%s: %v", err)
31 | os.Exit(2)
32 | }
33 |
34 | v := dkim.Verifier{}
35 | if err := v.Verify(context.Background(), bytes.NewReader(email)); err != nil {
36 | fmt.Fprintf(os.Stderr, "%s: %s\n", name, err)
37 | os.Exit(1)
38 | }
39 | fmt.Println("PASS")
40 | }
41 |
--------------------------------------------------------------------------------
/email/message.go:
--------------------------------------------------------------------------------
1 | // Package email is a light-weight set of types fundamental to processing email.
2 | package email
3 |
4 | import (
5 | "fmt"
6 | "io"
7 | "time"
8 | )
9 |
10 | // MsgID is a unique identifier for a message.
11 | //
12 | // MsgID is unique across all mailboxes.
13 | //
14 | // A message does not have a MsgID until it is stored in the client database.
15 | type MsgID int64
16 |
17 | func (id MsgID) String() string { return fmt.Sprintf("m%d", int64(id)) }
18 |
19 | // Msg is an email message.
20 | type Msg struct {
21 | MsgID MsgID // assigned on insertion into user mailbox, 0 otherwise
22 | Seed int64 // random used to seed multipart boundaries
23 | MailboxID int64 // assigned on insertion into user mailbox, 0 otherwise
24 | RawHash string
25 | Date time.Time // TODO: raw user Date, sanatized Date, or server recv date?
26 | Headers Header
27 | Flags []string
28 | Parts []Part // Parts[i].PartNum == i
29 | EncodedSize int64 // size of encoded message, IMAP value RFC822.SIZE
30 | }
31 |
32 | func (m *Msg) Close() {
33 | for _, p := range m.Parts {
34 | if p.Content != nil {
35 | p.Content.Close()
36 | p.Content = nil
37 | }
38 | }
39 | }
40 |
41 | // Part represents a single part of a MIME multipart message.
42 | // A Msg with a single text/plain part is not multipart encoded.
43 | type Part struct {
44 | PartNum int
45 | Name string
46 | IsBody bool
47 | IsAttachment bool
48 | IsCompressed bool // stored compressed on disk
49 | CompressedSize int64 // size of content when compressed if known
50 | ContentType string
51 | ContentID string
52 | Content Buffer // uncompressed data
53 | BlobID int64
54 |
55 | // TODO remove Path?
56 | Path string // MIME path as used in IMAP, ex. "1.2.3"
57 | ContentTransferEncoding string // "", "quoted-printable", "base64"
58 | ContentTransferSize int64 // transfer-encoded size
59 | ContentTransferLines int64 // transfer-encoded line count
60 | }
61 |
62 | // Buffer is content store.
63 | //
64 | // It is usually an *iox.BufferFile or *sqlite.Blob.
65 | //
66 | // Expect it to be fixed size.
67 | // TODO: remove io.Writer?
68 | // TODO: add io.ReaderAt?
69 | type Buffer interface {
70 | io.Reader
71 | io.Writer
72 | io.Seeker
73 | io.Closer
74 | Size() int64
75 | }
76 |
--------------------------------------------------------------------------------
/html/css/doc.go:
--------------------------------------------------------------------------------
1 | /*
2 | Package css implements a CSS tokenizer and parser.
3 | The parser is currently incomplete, it only covers declaration lists.
4 |
5 | It is written to the CSS Syntax Module Level 3 specification,
6 | https://www.w3.org/TR/css-syntax-3/.
7 | There are oddities in the spec so it is not taken as gospel.
8 | It suggests for example that declarations in style attributes
9 | can contain at-rules, when all other sources and implementations
10 | say they cannot.
11 | So this package was written by also consulting other sources,
12 | such as https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax.
13 |
14 | Scanner
15 |
16 | Turn bytes into tokens by calling the Next method until an EOF token:
17 |
18 | errh := func(line, col, n int, msg string) {
19 | log.Printf("%d:%d: %s", line, col, msg)
20 | }
21 | s := css.NewScanner(r, errh)
22 | for {
23 | s.Next()
24 | if s.Token == css.EOF {
25 | break
26 | }
27 | // ... process the token fields of s.
28 | }
29 |
30 | The error handler function errh will be called for CSS tokenization
31 | errors and any underlying I/O errors from the provided io.Reader.
32 |
33 | Note: []byte data provided by s is reused when Next is called.
34 |
35 | Parser
36 |
37 | An example of parsing a style attribute:
38 |
39 | errh := func(line, col, n int, msg string) {
40 | log.Printf("%d:%d: %s", line, col, msg)
41 | }
42 | p := css.NewParser(css.NewScanner(r, errh))
43 | var decl css.Decl
44 | for p.ParseDecl(&decl) {
45 | // A declaration is written to decl
46 | // and any parse errors are reported to errh.
47 | }
48 |
49 | */
50 | package css
51 |
--------------------------------------------------------------------------------
/html/css/format.go:
--------------------------------------------------------------------------------
1 | package css
2 |
3 | import (
4 | "strconv"
5 | )
6 |
7 | func appendEscapedString(dst, src []byte) []byte {
8 | for _, c := range src {
9 | switch c {
10 | case '\\':
11 | dst = append(dst, '\\', '\\')
12 | case '\n':
13 | dst = append(dst, '\\', '\n')
14 | case '"':
15 | dst = append(dst, '\\', '"')
16 | default:
17 | dst = append(dst, c)
18 | }
19 | }
20 | return dst
21 | }
22 |
23 | func AppendValue(dst []byte, v *Value) []byte {
24 | switch v.Type {
25 | case ValueIdent:
26 | dst = appendEscapedString(dst, v.Value)
27 | case ValueFunction:
28 | dst = appendEscapedString(dst, v.Value)
29 | dst = append(dst, '(')
30 | case ValueHash, ValueHashID:
31 | dst = append(dst, '#')
32 | dst = appendEscapedString(dst, v.Value)
33 | case ValueString:
34 | dst = append(dst, '"')
35 | dst = appendEscapedString(dst, v.Value)
36 | dst = append(dst, '"')
37 | case ValueURL:
38 | dst = append(dst, `url("`...)
39 | dst = appendEscapedString(dst, v.Value)
40 | dst = append(dst, `")`...)
41 | case ValueDelim:
42 | dst = appendEscapedString(dst, v.Value)
43 | case ValueNumber, ValueInteger:
44 | dst = strconv.AppendFloat(dst, v.Number, 'f', -1, 64)
45 | case ValuePercentage:
46 | dst = strconv.AppendFloat(dst, v.Number, 'f', -1, 64)
47 | dst = append(dst, '%')
48 | case ValueDimension:
49 | dst = strconv.AppendFloat(dst, v.Number, 'f', -1, 64)
50 | dst = append(dst, v.Value...)
51 | case ValueUnicodeRange:
52 | dst = append(dst, v.Value...)
53 | case ValueIncludeMatch:
54 | dst = append(dst, '~', '=')
55 | case ValueDashMatch:
56 | dst = append(dst, '|', '=')
57 | case ValuePrefixMatch:
58 | dst = append(dst, '^', '=')
59 | case ValueSuffixMatch:
60 | dst = append(dst, '$', '=')
61 | case ValueSubstringMatch:
62 | dst = append(dst, '*', '=')
63 | case ValueComma:
64 | dst = append(dst, ',')
65 | }
66 | return dst
67 | }
68 |
69 | func AppendDecl(dst []byte, d *Decl) []byte {
70 | dst = appendEscapedString(dst, d.Property)
71 | dst = append(dst, ':', ' ')
72 | for i := range d.Values {
73 | v := &d.Values[i]
74 | if i > 0 {
75 | switch v.Type {
76 | case ValueComma, ValueFunction, ValueDelim:
77 | default:
78 | switch d.Values[i-1].Type {
79 | case ValueFunction:
80 | default:
81 | dst = append(dst, ' ')
82 | }
83 | }
84 | }
85 | dst = AppendValue(dst, v)
86 | }
87 | dst = append(dst, ';')
88 | return dst
89 | }
90 |
--------------------------------------------------------------------------------
/html/css/format_test.go:
--------------------------------------------------------------------------------
1 | package css
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestAppendDecl(t *testing.T) {
8 | for _, test := range parseAndFormatDeclTests {
9 | t.Run(test.name, func(t *testing.T) {
10 | got := string(AppendDecl(nil, &test.decl[0]))
11 | if got != test.text {
12 | t.Errorf("\n got: %q\nwant: %q", got, test.text)
13 | }
14 | })
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/html/css/sourcereader.go:
--------------------------------------------------------------------------------
1 | package css
2 |
3 | import (
4 | "errors"
5 | "io"
6 | "unicode/utf8"
7 | )
8 |
9 | // TODO: independent tests of _SourceReader
10 | // TODO: does this deserve its own package? is it generally interesting?
11 |
12 | var _ErrMaxBufExceeded = errors.New("sourcereader: max buffer size exceeded")
13 |
14 | // _SourceReader reads runes from an io.Reader.
15 | // It provides methods designed for writing scanners.
16 | type _SourceReader struct {
17 | err error
18 | src io.Reader
19 | buf []byte // buffer. len-off is avail bytes, cap never exceeded
20 | off int // buf read offset
21 |
22 | line, col, n int
23 |
24 | recHasNULL bool // set if '\0' was seen while recording
25 | replaceNULL bool
26 | lastRuneLen int // number of bytes of buf in the last read rune, or -1
27 | lastCol int // value of col for the previous line
28 | }
29 |
30 | func _NewSourceReader(src io.Reader, maxBuf int) *_SourceReader {
31 | if maxBuf == 0 {
32 | maxBuf = 4096
33 | }
34 | return &_SourceReader{
35 | src: src,
36 | buf: make([]byte, 0, 4096),
37 | }
38 | }
39 |
40 | func (r *_SourceReader) fill() {
41 | // Slide unnecessary bytes to the beginning of the buffer to make space.
42 | slideOff := r.off
43 | if r.lastRuneLen > 0 {
44 | slideOff -= r.lastRuneLen // keep the last rune for unget
45 | }
46 | if slideOff > 0 {
47 | copy(r.buf, r.buf[slideOff:])
48 | r.buf = r.buf[:len(r.buf)-slideOff]
49 | r.off -= slideOff
50 | }
51 |
52 | if r.off == cap(r.buf) {
53 | r.err = _ErrMaxBufExceeded // no space to fill
54 | return
55 | }
56 |
57 | allbuf := r.buf[0:cap(r.buf)]
58 | n, err := r.src.Read(allbuf[len(r.buf):])
59 | r.buf = allbuf[:len(r.buf)+n]
60 | if err != nil {
61 | r.err = err
62 | } else if n == 0 {
63 | r.err = io.ErrNoProgress
64 | }
65 | }
66 |
67 | // SetReplaceNULL configures the _SourceReader to replace any '\0' runes with
68 | // the Unicode replacement character '\uFFFD'.
69 | func (r *_SourceReader) SetReplaceNULL(v bool) {
70 | r.replaceNULL = v
71 | }
72 |
73 | func (r *_SourceReader) Error() error {
74 | return r.err
75 | }
76 |
77 | func (r *_SourceReader) peek() (rn rune, size int) {
78 | r.fillTo(0)
79 | if r.off >= len(r.buf) {
80 | return -1, 0
81 | }
82 |
83 | size = 1
84 | rn = rune(r.buf[r.off])
85 | if rn >= utf8.RuneSelf {
86 | rn, size = utf8.DecodeRune(r.buf[r.off:])
87 | }
88 |
89 | if r.replaceNULL && rn == 0 {
90 | rn = '\uFFFD' // unicode replacement character
91 | }
92 |
93 | return rn, size
94 | }
95 |
96 | func (r *_SourceReader) PeekRune() rune {
97 | rn, _ := r.peek()
98 | return rn
99 | }
100 |
101 | func (r *_SourceReader) fillTo(peekOff int) {
102 | for r.err == nil {
103 | if r.off+peekOff+utf8.UTFMax <= len(r.buf) {
104 | break
105 | }
106 | if r.off+peekOff < len(r.buf) && utf8.FullRune(r.buf[r.off+peekOff:]) {
107 | break
108 | }
109 | r.fill()
110 | }
111 | }
112 |
113 | func (r *_SourceReader) PeekRunes(runes []rune) error {
114 | peekOff := 0
115 | for i := range runes {
116 | r.fillTo(peekOff)
117 | off := r.off + peekOff
118 | if off >= len(r.buf) {
119 | for i < len(runes) {
120 | runes[i] = -1
121 | i++
122 | }
123 | return r.err
124 | }
125 |
126 | size := 1
127 | rn := rune(r.buf[off])
128 | if rn >= utf8.RuneSelf {
129 | rn, size = utf8.DecodeRune(r.buf[off:])
130 | }
131 | if r.replaceNULL && rn == 0 {
132 | rn = '\uFFFD' // unicode replacement character
133 | }
134 | runes[i] = rn
135 | peekOff += size
136 | }
137 | return nil
138 | }
139 |
140 | // GetRune reads a single UTF-8 encoded character.
141 | // If an I/O error occurs reading, ReadRune returns -1.
142 | // The error is available from the Error method.
143 | func (r *_SourceReader) GetRune() rune {
144 | rn, size := r.peek()
145 | //println(fmt.Sprintf("GetRune rn=%s, size=%d", string(rn), size))
146 |
147 | if rn == -1 {
148 | return -1
149 | }
150 |
151 | r.lastRuneLen = size
152 | r.off += size
153 |
154 | if r.replaceNULL && rn == '\uFFFD' && size == 1 {
155 | r.recHasNULL = true
156 | }
157 |
158 | if rn == '\n' {
159 | r.lastCol = r.col
160 | r.line++
161 | r.col = 0
162 | } else {
163 | r.col += size
164 | }
165 | r.n += size
166 |
167 | return rn
168 | }
169 |
170 | // UngetRune unreads the last rune.
171 | // Only a single rune can be unread.
172 | func (r *_SourceReader) UngetRune() {
173 | if r.lastRuneLen < 0 {
174 | r.err = errors.New("sourcereader: no rune to unread")
175 | return
176 | }
177 | r.off -= r.lastRuneLen
178 | r.n -= r.lastRuneLen
179 | if r.col == 0 {
180 | r.col = r.lastCol
181 | r.line--
182 | } else {
183 | r.col -= r.lastRuneLen
184 | }
185 | r.lastRuneLen = -1
186 | }
187 |
188 | // Pos reports the line/column position and total bytes of the last read rune.
189 | // Column is a byte offset from the last '\n'.
190 | func (r *_SourceReader) Pos() (line, col, n int) {
191 | return r.line, r.col, r.n
192 | }
193 |
--------------------------------------------------------------------------------
/html/css/token_string.go:
--------------------------------------------------------------------------------
1 | // Code generated by "stringer -type Token"; DO NOT EDIT.
2 |
3 | package css
4 |
5 | import "strconv"
6 |
7 | const _Token_name = "UnknownEOFIdentFunctionAtKeywordHashStringBadStringURLBadURLDelimNumberPercentageDimensionUnicodeRangeIncludeMatchDashMatchPrefixMatchSuffixMatchSubstringMatchColumnCDOCDCColonSemicolonCommaLeftBrackRightBrackLeftParenRightParenLeftBraceRightBrace"
8 |
9 | var _Token_index = [...]uint8{0, 7, 10, 15, 23, 32, 36, 42, 51, 54, 60, 65, 71, 81, 90, 102, 114, 123, 134, 145, 159, 165, 168, 171, 176, 185, 190, 199, 209, 218, 228, 237, 247}
10 |
11 | func (i Token) String() string {
12 | if i >= Token(len(_Token_index)-1) {
13 | return "Token(" + strconv.FormatInt(int64(i), 10) + ")"
14 | }
15 | return _Token_name[_Token_index[i]:_Token_index[i+1]]
16 | }
17 |
--------------------------------------------------------------------------------
/html/css/typeflag_string.go:
--------------------------------------------------------------------------------
1 | // Code generated by "stringer -type TypeFlag"; DO NOT EDIT.
2 |
3 | package css
4 |
5 | import "strconv"
6 |
7 | const _TypeFlag_name = "TypeFlagNoneTypeFlagIDTypeFlagNumberTypeFlagInteger"
8 |
9 | var _TypeFlag_index = [...]uint8{0, 12, 22, 36, 51}
10 |
11 | func (i TypeFlag) String() string {
12 | if i >= TypeFlag(len(_TypeFlag_index)-1) {
13 | return "TypeFlag(" + strconv.FormatInt(int64(i), 10) + ")"
14 | }
15 | return _TypeFlag_name[_TypeFlag_index[i]:_TypeFlag_index[i+1]]
16 | }
17 |
--------------------------------------------------------------------------------
/html/css/valuetype_string.go:
--------------------------------------------------------------------------------
1 | // Code generated by "stringer -type ValueType -linecomment"; DO NOT EDIT.
2 |
3 | package css
4 |
5 | import "strconv"
6 |
7 | const _ValueType_name = "ValueUknownidentfunctionhashhash-idstringurldelimnumint TODO removepercentdimunicode-rangeinclude-matchdash-matchprefix-matchsuffix-matchsubstr-matchcomma"
8 |
9 | var _ValueType_index = [...]uint8{0, 11, 16, 24, 28, 35, 41, 44, 49, 52, 67, 74, 77, 90, 103, 113, 125, 137, 149, 154}
10 |
11 | func (i ValueType) String() string {
12 | if i < 0 || i >= ValueType(len(_ValueType_index)-1) {
13 | return "ValueType(" + strconv.FormatInt(int64(i), 10) + ")"
14 | }
15 | return _ValueType_name[_ValueType_index[i]:_ValueType_index[i+1]]
16 | }
17 |
--------------------------------------------------------------------------------
/html/htmlembed/htmlembed.go:
--------------------------------------------------------------------------------
1 | // Package htmlembed fetches all the assets referenced in an HTML document.
2 | package htmlembed
3 |
4 | import (
5 | "context"
6 | "crypto/sha256"
7 | "encoding/base64"
8 | "fmt"
9 | "io"
10 | "net/http"
11 | "net/url"
12 | "path"
13 | "time"
14 |
15 | "crawshaw.io/iox"
16 | "golang.org/x/net/html"
17 | "spilled.ink/html/htmlsafe"
18 | )
19 |
20 | type Embedder struct {
21 | userAgent string
22 | filer *iox.Filer
23 | httpc Doer
24 | }
25 |
26 | type Doer interface {
27 | Do(*http.Request) (*http.Response, error)
28 | }
29 |
30 | func NewEmbedder(filer *iox.Filer, httpc Doer) *Embedder {
31 | return &Embedder{
32 | userAgent: "Spilled_Ink_FetchBot/1.0",
33 | filer: filer,
34 | httpc: httpc,
35 | }
36 | }
37 |
38 | type Asset struct {
39 | CID string
40 | URL string
41 | Name string
42 | ContentType string
43 | Hash [sha256.Size]byte
44 | Bytes *iox.BufferFile
45 | LoadError error
46 | }
47 |
48 | type HTML struct {
49 | HTML *iox.BufferFile
50 | Assets []Asset
51 | Links []string
52 | }
53 |
54 | type work struct {
55 | n *html.Node
56 | url string
57 | }
58 |
59 | // Embed returns a sanitized version of a block of HTML with any
60 | // external resources collected in the style of multipart/mixed.
61 | func (p *Embedder) Embed(ctx context.Context, r io.Reader) (res *HTML, err error) {
62 | const maxRead = 1 << 19
63 | r = io.LimitReader(r, maxRead)
64 |
65 | res = &HTML{}
66 |
67 | buf := p.filer.BufferFile(0)
68 | defer buf.Close()
69 |
70 | // First pass: collect assets to fetch.
71 | rewriteFn := func(attr string, url *url.URL) string {
72 | if attr == "href" {
73 | return url.String()
74 | }
75 | if url.Scheme == "cid" {
76 | return url.String()
77 | }
78 | cid := fmt.Sprintf("fetchasset%d", len(res.Assets))
79 | res.Assets = append(res.Assets, Asset{
80 | CID: cid,
81 | URL: url.String(),
82 | Name: path.Base(url.Path),
83 | Bytes: p.filer.BufferFile(0),
84 | })
85 | return "cid:" + cid
86 | }
87 | s := &htmlsafe.Sanitizer{
88 | RewriteURL: rewriteFn,
89 | Options: htmlsafe.Safe,
90 | MaxBuf: maxRead,
91 | }
92 | if _, err := s.Sanitize(buf, r); err != nil {
93 | return nil, err // I/O error
94 | }
95 | if _, err := buf.Seek(0, 0); err != nil {
96 | return nil, err
97 | }
98 |
99 | done := make(chan struct{}, len(res.Assets))
100 | ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
101 | defer cancel()
102 | for i := range res.Assets {
103 | a := &res.Assets[i]
104 | go func() {
105 | defer func() {
106 | done <- struct{}{}
107 | }()
108 | p.fetch(ctx, a)
109 | }()
110 | }
111 | for range res.Assets {
112 | <-done
113 | }
114 |
115 | // Second pass. Rename assets with unique hash ID.
116 | rewriteFn = func(attr string, url *url.URL) string {
117 | if url.Scheme != "cid" {
118 | return url.String()
119 | }
120 | var assetNum int
121 | if _, err := fmt.Sscanf(url.String(), "cid:fetchasset%d", &assetNum); err != nil {
122 | return url.String()
123 | }
124 | if assetNum < 0 || assetNum > len(res.Assets) {
125 | return url.String() // uh oh
126 | }
127 |
128 | hash := res.Assets[assetNum].Hash[:]
129 | cid := fmt.Sprintf("%s@spilled.ink", base64.URLEncoding.EncodeToString(hash))
130 | res.Assets[assetNum].CID = cid
131 |
132 | return "cid:" + cid
133 | }
134 | res.HTML = p.filer.BufferFile(0)
135 | s = &htmlsafe.Sanitizer{
136 | RewriteURL: rewriteFn,
137 | Options: htmlsafe.Safe,
138 | MaxBuf: maxRead,
139 | }
140 | if _, err := s.Sanitize(res.HTML, buf); err != nil {
141 | return nil, err // I/O error
142 | }
143 | if _, err := res.HTML.Seek(0, 0); err != nil {
144 | return nil, err
145 | }
146 |
147 | return res, nil
148 | }
149 |
150 | func (p *Embedder) fetch(ctx context.Context, a *Asset) {
151 | defer func() {
152 | if a.LoadError != nil && a.Bytes != nil {
153 | a.Bytes.Close()
154 | a.Bytes = nil
155 | }
156 | }()
157 |
158 | req, err := http.NewRequest("GET", a.URL, nil)
159 | if err != nil {
160 | a.LoadError = err
161 | return
162 | }
163 | req.Header.Set("User-Agent", p.userAgent)
164 | res, err := p.httpc.Do(req.WithContext(ctx))
165 | if err != nil {
166 | a.LoadError = err
167 | return
168 | }
169 | defer func() {
170 | if err := res.Body.Close(); a.LoadError == nil {
171 | a.LoadError = err
172 | }
173 | }()
174 | if res.StatusCode != 200 {
175 | a.LoadError = fmt.Errorf("%d: %s", res.StatusCode, res.Status)
176 | return
177 | }
178 | h := sha256.New()
179 | if _, err := io.Copy(a.Bytes, io.TeeReader(res.Body, h)); err != nil {
180 | a.LoadError = fmt.Errorf("body copy failed %v", err)
181 | return
182 | }
183 | h.Sum(a.Hash[:0])
184 | a.Bytes.Seek(0, 0)
185 |
186 | a.ContentType = res.Header.Get("Content-Type")
187 | if a.ContentType == "" {
188 | bufPrefix := make([]byte, 512)
189 | n, err := io.ReadAtLeast(a.Bytes, bufPrefix, len(bufPrefix))
190 | if err != nil && err != io.ErrUnexpectedEOF {
191 | a.Bytes.Close()
192 | a.Bytes = nil
193 | a.LoadError = fmt.Errorf("content-type detection failed: %v", err)
194 | return
195 | }
196 | a.Bytes.Seek(0, 0)
197 | bufPrefix = bufPrefix[:n]
198 | a.ContentType = http.DetectContentType(bufPrefix)
199 | }
200 |
201 | if a.ContentType == "image/jpg" { // sigh
202 | a.ContentType = "image/jpeg"
203 | }
204 |
205 | switch a.ContentType {
206 | case "image/gif",
207 | "image/jpeg",
208 | "image/png",
209 | "image/svg+xml":
210 | default:
211 | a.LoadError = fmt.Errorf("unexpected content-type: %v", a.ContentType)
212 | }
213 | }
214 |
--------------------------------------------------------------------------------
/html/htmlembed/htmlembed_test.go:
--------------------------------------------------------------------------------
1 | package htmlembed
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/base64"
7 | "io"
8 | "io/ioutil"
9 | "net/http"
10 | "net/http/httptest"
11 | "strings"
12 | "testing"
13 |
14 | "crawshaw.io/iox"
15 | )
16 |
17 | // TODO: include a repeated URL
18 | // TODO: check a ref="nofollow"
19 | // TODO: we can do better for the cid of missing content
20 |
21 | const msgText = `Rich text. Have some images:
22 |
23 |
24 |
25 | `
26 |
27 | var wantText = `Rich text. Have some images:
28 |
29 |
30 |
31 | `
32 |
33 | var testAssets = []struct {
34 | name string
35 | contents string
36 | loaderror string
37 | cid string
38 | }{
39 | {
40 | name: "foo.gif",
41 | contents: `R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAEALAAAAAABAAEAAAgEAAMEBAA7`,
42 | cid: "Ys-wVAiOKaDldrQ0AwwjbGEBrwWZ5vVc_omzWmGG-6Q=@spilled.ink",
43 | },
44 | {
45 | name: "bar.gif",
46 | contents: `R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7`,
47 | cid: "7xlVrnV8i5ZsgySDUDMb06MPZYztEfOH-OvwWrM2hik=@spilled.ink",
48 | },
49 | {
50 | name: "doesnotexist.gif",
51 | loaderror: "404",
52 | },
53 | }
54 |
55 | func TestEmbedder(t *testing.T) {
56 | ctx, cancel := context.WithCancel(context.Background())
57 | filer := iox.NewFiler(0)
58 | defer filer.Shutdown(ctx)
59 | defer cancel()
60 |
61 | s := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
62 | var errstr string
63 | out := make([]byte, 0, 128)
64 | for _, a := range testAssets {
65 | if a.name == r.URL.Path[1:] {
66 | n, err := base64.StdEncoding.Decode(out[:cap(out)], []byte(a.contents))
67 | if err != nil {
68 | t.Fatalf("%s decode: %v", a.name, err)
69 | }
70 | out = out[:n]
71 | errstr = a.loaderror
72 | break
73 | }
74 | }
75 | if errstr != "" {
76 | http.Error(w, "not found", 404)
77 | return
78 | }
79 | if len(out) == 0 {
80 | t.Errorf("unknown http server path: %s", r.URL.String())
81 | http.Error(w, "missing", 500)
82 | return
83 | }
84 | w.Write(out)
85 | }))
86 |
87 | msgText := strings.Replace(msgText, "https://example.com", s.URL, -1)
88 | e := NewEmbedder(filer, s.Client())
89 | res, err := e.Embed(ctx, strings.NewReader(msgText))
90 | if err != nil {
91 | t.Fatal(err)
92 | }
93 | if len(res.Assets) != len(testAssets) {
94 | t.Fatalf("len(res.Assets)=%d, want 2", len(res.Assets))
95 | }
96 |
97 | gotHTML, err := ioutil.ReadAll(res.HTML)
98 | if err != nil {
99 | t.Fatal(err)
100 | }
101 | if got, want := string(gotHTML), wantText; got != want {
102 | t.Errorf("HTML=%q\nwant=%q", got, want)
103 | }
104 |
105 | for i, asset := range res.Assets {
106 | a := testAssets[i]
107 | if asset.LoadError != nil {
108 | if a.loaderror == "" {
109 | t.Errorf("%s: unexpected load error: %v", a.name, asset.LoadError)
110 | continue
111 | }
112 | if !strings.Contains(asset.LoadError.Error(), a.loaderror) {
113 | t.Errorf("%s load error %v does not mention %q", a.name, asset.LoadError, a.loaderror)
114 | }
115 | continue
116 | }
117 | if a.loaderror != "" {
118 | t.Errorf("%s: missing load error: %q", a.name, a.loaderror)
119 | continue
120 | }
121 | if got, want := mustBase64(t, a.name, asset.Bytes), a.contents; got != want {
122 | t.Errorf("%s=%q, want %q", a.name, got, want)
123 | }
124 | if a.cid != asset.CID {
125 | t.Errorf("%s CID=%s, want %s", a.name, asset.CID, a.cid)
126 | }
127 | }
128 | }
129 |
130 | func mustBase64(t *testing.T, name string, r io.Reader) string {
131 | buf := new(bytes.Buffer)
132 | wc := base64.NewEncoder(base64.StdEncoding, buf)
133 | if _, err := io.Copy(wc, r); err != nil {
134 | t.Fatalf("copying %s: %v", name, err)
135 | }
136 | if err := wc.Close(); err != nil {
137 | t.Fatalf("closing copy of %s: %v", name, err)
138 | }
139 | return buf.String()
140 | }
141 |
--------------------------------------------------------------------------------
/html/htmlsafe/htmlsafe_test.go:
--------------------------------------------------------------------------------
1 | package htmlsafe
2 |
3 | import (
4 | "bytes"
5 | "net/url"
6 | "strings"
7 | "testing"
8 | )
9 |
10 | var sanitizeTests = []struct {
11 | name string
12 | in string
13 | out string
14 | err error
15 | opts *Options
16 | rurl func(attr string, url *url.URL) string
17 | }{
18 | {
19 | name: "banal-strict",
20 | in: "hellohello
world
",
21 | out: "hello
world
",
22 | opts: StrictEmail,
23 | },
24 | {
25 | name: "banal-safe",
26 | in: "hellohello
world
",
27 | out: "hellohello
world
",
28 | opts: Safe,
29 | },
30 | {
31 | name: "script",
32 | in: `one three
`,
33 | out: `one three
`,
34 | },
35 | {
36 | name: "script-in-script",
37 | in: `one --> three
`,
38 | out: `one three
`,
39 | },
40 | {
41 | name: "script-in-pre",
42 | in: `one
three
`,
43 | out: `onethree
`,
44 | },
45 | {
46 | name: "filter styles",
47 | in: ``,
48 | out: ``,
49 | },
50 | {
51 | name: "style quoting",
52 | in: ``,
53 | out: ``,
54 | },
55 | {
56 | name: "tag in attr", // TODO: should we escape this? <
57 | in: ``,
58 | out: ``,
59 | },
60 | {
61 | name: "remove attr",
62 | in: `hi`,
63 | out: `hi`,
64 | },
65 | {
66 | name: "remove js links",
67 | in: `123`,
68 | out: `123`,
69 | },
70 | {
71 | name: "keep external links by default",
72 | in: `ex
`,
73 | out: `ex
`,
74 | },
75 | {
76 | name: "remove bad src",
77 | in: `;)
`,
78 | out: `![]()
`,
79 | },
80 | {
81 | name: "keep cid src",
82 | in: `
`,
83 | out: `
`,
84 | },
85 | {
86 | name: "escape link attrs",
87 | in: `
`,
88 | out: `
`,
89 | },
90 | {
91 | name: "replace URLs",
92 | in: `
`,
93 | out: `
`,
94 | rurl: func(attr string, url *url.URL) string {
95 | return "https://example.com" + url.Path
96 | },
97 | },
98 | }
99 |
100 | func TestSanitize(t *testing.T) {
101 | for _, test := range sanitizeTests {
102 | t.Run(test.name, func(t *testing.T) {
103 | s := Sanitizer{
104 | RewriteURL: test.rurl,
105 | Options: test.opts,
106 | }
107 | src := strings.NewReader(test.in)
108 | dst := new(bytes.Buffer)
109 |
110 | n, err := s.Sanitize(dst, src)
111 | if err != test.err {
112 | t.Errorf("got err %v, want %v", err, test.err)
113 | }
114 | if n != dst.Len() {
115 | t.Errorf("n=%d want %d", n, dst.Len())
116 | }
117 | if got := dst.String(); got != test.out {
118 | t.Errorf("Sanitize(%q)\n\t = %q,\n\twant %q", test.in, got, test.out)
119 | }
120 | })
121 | }
122 | }
123 |
124 | func BenchmarkSanitize(b *testing.B) {
125 | for _, test := range sanitizeTests {
126 | b.Run(test.name, func(b *testing.B) {
127 | s := Sanitizer{Options: test.opts}
128 | src := strings.NewReader(test.in)
129 | dst := new(bytes.Buffer)
130 | dst.Grow(len(test.in))
131 |
132 | b.ReportAllocs()
133 | for i := 0; i < b.N; i++ {
134 | src.Reset(test.in)
135 | dst.Truncate(0)
136 |
137 | if _, err := s.Sanitize(dst, src); err != nil {
138 | b.Fatal(err)
139 | }
140 | }
141 | })
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/imap/imap.go:
--------------------------------------------------------------------------------
1 | // Package imap defines the core types used by the IMAP server.
2 | //
3 | // TODO document
4 | // TODO remove imapparser dependency?
5 | package imap
6 |
7 | import (
8 | "io"
9 | "sort"
10 | "time"
11 |
12 | "spilled.ink/email"
13 | "spilled.ink/imap/imapparser"
14 | )
15 |
16 | // Session is an authenticated user session to the IMAP server.
17 | type Session interface {
18 | Mailboxes() ([]MailboxSummary, error)
19 | Mailbox(name []byte) (Mailbox, error)
20 | CreateMailbox(name []byte, attr ListAttrFlag) error
21 | DeleteMailbox(name []byte) error
22 | RenameMailbox(old, new []byte) error
23 | RegisterPushDevice(name string, device imapparser.ApplePushDevice) error
24 | Close()
25 | }
26 |
27 | // Mailbox is an open user mailbox in a session.
28 | type Mailbox interface {
29 | // ID is an immutable positive number.
30 | // If the name of the Mailbox is changed, the ID remains unchanged.
31 | ID() int64
32 |
33 | Info() (MailboxInfo, error)
34 |
35 | // Append appends a message to the mailbox.
36 | Append(flags [][]byte, date time.Time, data io.ReadSeeker) (uid uint32, err error)
37 |
38 | // Search finds all messages that match op and calls fn for each one.
39 | Search(op *imapparser.SearchOp, fn func(MessageSummary)) error
40 |
41 | // Fetch fetches the messages named by seqs and calls fn for each one.
42 | //
43 | // If uid is true then seqs is a set of UIDs, otherwise
44 | // it is a set of sequence numbers
45 | //
46 | // The Message passed to fn may have a nil Content for all parts.
47 | // If the imapserver needs the content it will call LoadPart.
48 | //
49 | // The Message is only valid for the duration of the call to fn.
50 | //
51 | // Fetch must Close the email.Msg after fn returns.
52 | Fetch(uid bool, seqs []imapparser.SeqRange, changedSince int64, fn func(Message)) error
53 |
54 | // Expunge deleted all messages in the mailbox with the \Deleted flag.
55 | //
56 | // If uidSeqs is non-nil then only messages whose UID matches and
57 | // have the \Deleted flag are expunged.
58 | //
59 | // If fn is non-nil it is called with the seqNum for each deleted
60 | // message. The sequence numbers follow the amazing rules of the IMAP
61 | // expunge command, that is, each is reported after the previous
62 | // is removed and the sequence numbers recalculated.
63 | Expunge(uidSeqs []imapparser.SeqRange, fn func(seqNum uint32)) error
64 |
65 | Store(uid bool, seqs []imapparser.SeqRange, store *imapparser.Store) (StoreResults, error)
66 |
67 | // Move moves messages from this mailbox to dst.
68 | //
69 | // Each message moved is reported by calling fn.
70 | Move(uid bool, seqs []imapparser.SeqRange, dst Mailbox, fn func(seqNum, srcUID, dstUID uint32)) error
71 |
72 | // Copies moves messages from this mailbox to dst.
73 | //
74 | // Each message copied is reported by calling fn.
75 | Copy(uid bool, seqs []imapparser.SeqRange, dst Mailbox, fn func(srcUID, dstUID uint32)) error
76 |
77 | HighestModSequence() (int64, error) // TODO: just use Info?
78 |
79 | Close() error
80 | }
81 |
82 | type MailboxSummary struct {
83 | Name string
84 | Attrs ListAttrFlag
85 | }
86 |
87 | type MailboxInfo struct {
88 | Summary MailboxSummary
89 | // TODO Flags
90 | NumMessages uint32
91 | NumRecent uint32
92 | NumUnseen uint32
93 | UIDNext uint32 // must be greater than zero
94 | UIDValidity uint32 // must be greater than zero
95 | FirstUnseenSeqNum uint32
96 | HighestModSequence int64
97 | }
98 |
99 | type StoreResult struct {
100 | SeqNum uint32
101 | UID uint32
102 | Flags []string
103 | ModSequence int64
104 | }
105 |
106 | type StoreResults struct {
107 | Stored []StoreResult
108 | FailedModified []imapparser.SeqRange
109 | }
110 |
111 | // TODO type Seqs struct { UID bool; Seqs []imapparser.SeqRange } ?
112 |
113 | type MessageSummary struct {
114 | SeqNum uint32
115 | UID uint32
116 | ModSeq int64
117 | }
118 |
119 | type Message interface {
120 | Summary() MessageSummary
121 |
122 | // Msg returns the email.Msg.
123 | // Subsequent calls to Msg return the same memory.
124 | Msg() *email.Msg
125 |
126 | // TODO: conditional LoadPartsSummary in Fetch.
127 | // Reduces number of SQL queries from O(n) to O(1) in easy cases.
128 |
129 | // LoadPart loads Msg().Part[partNum].Content.
130 | //
131 | // Any subsequent calls to Msg will return the part with content
132 | // as long as Message is valid.
133 | LoadPart(partNum int) error
134 |
135 | // SetSeen sets the \Seen flag on this message.
136 | SetSeen() error
137 | }
138 |
139 | type Notifier interface {
140 | Notify(userID int64, mailboxID int64, mailboxName string, devices []imapparser.ApplePushDevice)
141 | }
142 |
143 | type ListAttrFlag int
144 |
145 | const (
146 | AttrNone ListAttrFlag = 0
147 | AttrNoinferiors ListAttrFlag = 1 << iota
148 | AttrNoselect
149 | AttrMarked
150 | AttrUnmarked
151 |
152 | // SPECIAL-USE mailbox attributes, RFC 6164
153 | AttrAll
154 | AttrArchive
155 | AttrDrafts
156 | AttrFlagged
157 | AttrJunk
158 | AttrSent
159 | AttrTrash
160 | )
161 |
162 | func (attrs ListAttrFlag) String() (res string) {
163 | for _, attr := range attrList {
164 | if attrs&attr != 0 {
165 | s := attrStrings[attr]
166 | if res == "" {
167 | res = s
168 | } else {
169 | res = res + " " + s
170 | }
171 | }
172 | }
173 | return res
174 | }
175 |
176 | var attrStrings = map[ListAttrFlag]string{
177 | AttrNoinferiors: `\Noinferiors`,
178 | AttrNoselect: `\Noselect`,
179 | AttrMarked: `\Marked`,
180 | AttrUnmarked: `\Unmarked`,
181 | AttrAll: `\All`,
182 | AttrArchive: `\Archive`,
183 | AttrDrafts: `\Drafts`,
184 | AttrFlagged: `\Flagged`,
185 | AttrJunk: `\Junk`,
186 | AttrSent: `\Sent`,
187 | AttrTrash: `\Trash`,
188 | }
189 |
190 | var attrList = func() (attrList []ListAttrFlag) {
191 | for attr := range attrStrings {
192 | attrList = append(attrList, attr)
193 | }
194 | sort.Slice(attrList, func(i, j int) bool { return attrList[i] < attrList[j] })
195 | return attrList
196 | }()
197 |
--------------------------------------------------------------------------------
/imap/imapparser/scanner_test.go:
--------------------------------------------------------------------------------
1 | package imapparser
2 |
3 | import (
4 | "bufio"
5 | "fmt"
6 | "reflect"
7 | "strings"
8 | "testing"
9 | "unicode"
10 | )
11 |
12 | type tok struct {
13 | t Token
14 | v string
15 | s []SeqRange
16 | }
17 |
18 | func (t tok) String() string {
19 | return fmt.Sprintf("{%s %q %v}", t.t, t.v, t.s)
20 | }
21 |
22 | var scannerTests = []struct {
23 | name string
24 | input string
25 | expects map[int]Token
26 | output []tok
27 | errstr string
28 | }{
29 | {
30 | input: "\r\n",
31 | output: []tok{{t: TokenEnd}},
32 | },
33 | {
34 | input: `SELECT "My \"Drafts\": \\o/"` + "\r\n",
35 | output: []tok{
36 | {t: TokenAtom, v: "SELECT"},
37 | {t: TokenString, v: `My "Drafts": \o/`},
38 | {t: TokenEnd},
39 | },
40 | },
41 | {
42 | input: `"unterminated`,
43 | output: []tok{},
44 | errstr: "unterminated string",
45 | },
46 | {
47 | input: `"unterminated\`,
48 | output: []tok{},
49 | errstr: "unterminated string",
50 | },
51 | {
52 | input: "3 UID SEARCH 1:* NOT DELETED\r\n",
53 | expects: map[int]Token{
54 | 0: TokenTag,
55 | 3: TokenSequences,
56 | },
57 | output: []tok{
58 | {t: TokenTag, v: "3"}, // 0
59 | {t: TokenAtom, v: "UID"},
60 | {t: TokenAtom, v: "SEARCH"},
61 | {t: TokenSequences, s: []SeqRange{{Min: 1, Max: 0}}}, // 3
62 | {t: TokenAtom, v: "NOT"},
63 | {t: TokenAtom, v: "DELETED"},
64 | {t: TokenEnd},
65 | },
66 | },
67 | {
68 | name: "atoms cannot contain ']'",
69 | input: "[3]",
70 | output: []tok{},
71 | errstr: "invalid atom character",
72 | },
73 | {
74 | input: "[3] NOOP\r\n",
75 | expects: map[int]Token{
76 | 0: TokenTag,
77 | },
78 | output: []tok{
79 | {t: TokenTag, v: "[3]"},
80 | {t: TokenAtom, v: "NOOP"},
81 | {t: TokenEnd},
82 | },
83 | },
84 | {
85 | name: "atoms can contain '+' but tags can not",
86 | input: "7+ 7+\r\n",
87 | expects: map[int]Token{
88 | 1: TokenTag,
89 | },
90 | output: []tok{
91 | {t: TokenAtom, v: "7+"},
92 | },
93 | errstr: "invalid tag character",
94 | },
95 | {
96 | input: "2,4:7,9,12:* 15 9:3 *\r\n",
97 | expects: map[int]Token{
98 | 0: TokenSequences,
99 | 1: TokenSequences,
100 | 2: TokenSequences,
101 | 3: TokenSequences,
102 | },
103 | output: []tok{
104 | {t: TokenSequences, s: []SeqRange{
105 | {Min: 2, Max: 2},
106 | {Min: 4, Max: 7},
107 | {Min: 9, Max: 9},
108 | {Min: 12, Max: 0},
109 | }},
110 | {t: TokenSequences, s: []SeqRange{{Min: 15, Max: 15}}},
111 | {t: TokenSequences, s: []SeqRange{{Min: 3, Max: 9}}},
112 | {t: TokenSequences, s: []SeqRange{{Min: 0, Max: 0}}},
113 | {t: TokenEnd},
114 | },
115 | },
116 | {
117 | name: "short literal",
118 | input: "{4}\r\n💩\r\n",
119 | expects: map[int]Token{
120 | 0: TokenString,
121 | },
122 | output: []tok{
123 | {t: TokenString, v: "💩"},
124 | {t: TokenEnd},
125 | },
126 | },
127 | {
128 | name: "short literal limit",
129 | input: "{2048}\r\n" + string(make([]byte, 2048)) + "\r\n",
130 | expects: map[int]Token{
131 | 0: TokenString,
132 | },
133 | output: []tok{},
134 | errstr: "greater than max 1024",
135 | },
136 | }
137 |
138 | func TestScanner(t *testing.T) {
139 | for _, test := range scannerTests {
140 | name := test.name
141 | if name == "" {
142 | name = test.input
143 | }
144 | t.Run(name, func(t *testing.T) {
145 | r := bufio.NewReader(strings.NewReader(test.input))
146 | f := filer.BufferFile(1024)
147 | defer f.Close()
148 | s := NewScanner(r, f, nil)
149 | got := []tok{}
150 | i := 0
151 | const limit = 1000
152 | for ; i < limit && s.Next(test.expects[i]); i++ {
153 | token := tok{
154 | t: s.Token,
155 | v: string(s.Value),
156 | s: append(([]SeqRange)(nil), s.Sequences...),
157 | }
158 | got = append(got, token)
159 | }
160 | if i == limit {
161 | t.Error("limit overrun")
162 | }
163 | if rem := s.buf.Buffered(); s.Error == nil && rem > 0 {
164 | t.Errorf("unscanned bytes, %d remaining", rem)
165 | }
166 | errstr := ""
167 | if s.Error != nil {
168 | errstr = s.Error.Error()
169 | }
170 | if !strings.Contains(errstr, test.errstr) {
171 | t.Errorf("scanner.Error=%q, want substring %q", s.Error, test.errstr)
172 | }
173 | if !reflect.DeepEqual(got, test.output) {
174 | t.Errorf("scanner\n got: %v\nwant: %v", got, test.output)
175 | }
176 | if s.Next(TokenUnknown) {
177 | t.Errorf("trailing token: %v", s.Token)
178 | }
179 | })
180 | }
181 | }
182 |
183 | func Test7bitPrint(t *testing.T) {
184 | for b := byte(0); b < 0x7f; b++ {
185 | if want, got := unicode.IsPrint(rune(b)), is7bitPrint(b); want != got {
186 | t.Errorf("is7bitPrint(%d)=%v but unicdoe.IsPrint(%d)=%v", b, got, b, want)
187 | }
188 | }
189 | }
190 |
--------------------------------------------------------------------------------
/imap/imapparser/search.go:
--------------------------------------------------------------------------------
1 | package imapparser
2 |
3 | import (
4 | "strings"
5 | "time"
6 | )
7 |
8 | type MatchMessage interface {
9 | SeqNum() uint32
10 | UID() uint32
11 | ModSeq() int64
12 | Flag(name string) bool
13 | Header(name string) string
14 | Date() time.Time
15 | RFC822Size() int64
16 | }
17 |
18 | type Matcher struct {
19 | op *SearchOp
20 | }
21 |
22 | func NewMatcher(op *SearchOp) (*Matcher, error) {
23 | // TODO: check keys are valid
24 | return &Matcher{op: op}, nil
25 | }
26 |
27 | func (m *Matcher) Match(msg MatchMessage) bool {
28 | return m.match(msg, m.op)
29 | }
30 |
31 | func (m *Matcher) match(msg MatchMessage, op *SearchOp) bool {
32 | switch op.Key {
33 | case "AND":
34 | for _, op := range op.Children {
35 | if !m.match(msg, &op) {
36 | return false
37 | }
38 | }
39 | return true
40 | case "OR":
41 | for _, op := range op.Children {
42 | if m.match(msg, &op) {
43 | return true
44 | }
45 | }
46 | return false
47 | case "SEQSET":
48 | return SeqContains(op.Sequences, msg.SeqNum())
49 | case "UID":
50 | return SeqContains(op.Sequences, msg.UID())
51 | case "ALL":
52 | return true
53 | case "BEFORE":
54 | return msg.Date().Before(op.Date)
55 | case "KEYWORD":
56 | // TODO
57 | case "LARGER":
58 | return msg.RFC822Size() > op.Num
59 | case "SMALLER":
60 | return msg.RFC822Size() < op.Num
61 | case "MODSEQ":
62 | return msg.ModSeq() >= op.Num
63 | case "NEW":
64 | // equivalent to (RECENT UNSEEN)
65 | return msg.Flag(`\Recent`) && !msg.Flag(`\Seen`)
66 | case "NOT":
67 | if len(op.Children) != 1 {
68 | return false // bad AST, avoid panic
69 | }
70 | return !m.match(msg, &op.Children[0])
71 | case "OLD":
72 | return !msg.Flag(`\Recent`)
73 | case "ON":
74 | // Ignore time.
75 | year, month, day := msg.Date().Date()
76 | return time.Date(year, month, day, 0, 0, 0, 0, time.UTC).Equal(op.Date)
77 | case "RECENT":
78 | return msg.Flag(`\Recent`)
79 | case "SEEN":
80 | return msg.Flag(`\Seen`)
81 | case "SENTBEFORE":
82 | // TODO
83 | case "SENTON":
84 | // TODO
85 | case "SENTSINCE":
86 | // TODO
87 | case "SINCE":
88 | year, month, day := msg.Date().Date()
89 | t := time.Date(year, month, day, 0, 0, 0, 0, time.UTC)
90 | return t.Equal(op.Date) || t.After(op.Date)
91 | case "HEADER":
92 | i := strings.IndexByte(op.Value, ':')
93 | if i < 1 {
94 | return false
95 | }
96 | name := op.Value[:i]
97 | value := ""
98 | if i < len(op.Value)-1 {
99 | value = op.Value[i+2:]
100 | }
101 | return msg.Header(name) == value
102 | case "SUBJECT":
103 | return strings.Contains(msg.Header("Subject"), op.Value)
104 | case "TO":
105 | return strings.Contains(msg.Header("To"), op.Value)
106 | case "FROM":
107 | return strings.Contains(msg.Header("From"), op.Value)
108 | case "CC":
109 | return strings.Contains(msg.Header("CC"), op.Value)
110 | case "BCC":
111 | return strings.Contains(msg.Header("BCC"), op.Value)
112 | case "BODY":
113 | // TODO
114 | case "TEXT":
115 | // TODO
116 | case "ANSWERED":
117 | return msg.Flag(`\Answered`)
118 | case "UNANSWERED":
119 | return !msg.Flag(`\Answered`)
120 | case "DELETED":
121 | return msg.Flag(`\Deleted`)
122 | case "UNDELETED":
123 | return !msg.Flag(`\Deleted`)
124 | case "DRAFT":
125 | return msg.Flag(`\Draft`)
126 | case "UNDRAFT":
127 | return !msg.Flag(`\Draft`)
128 | case "FLAGGED":
129 | return msg.Flag(`\Flagged`)
130 | case "UNFLAGGED":
131 | return !msg.Flag(`\Flagged`)
132 | case "UNKEYWORD":
133 | // TODO
134 | case "UNSEEN":
135 | return !msg.Flag(`\Seen`)
136 | }
137 | return false
138 | }
139 |
140 | func SeqContains(sequences []SeqRange, seqNum uint32) bool {
141 | for _, seq := range sequences {
142 | if seq.Min <= seqNum && (seq.Max == 0 || seq.Max >= seqNum) {
143 | return true
144 | }
145 | }
146 | return false
147 | }
148 |
--------------------------------------------------------------------------------
/imap/imapparser/search_test.go:
--------------------------------------------------------------------------------
1 | package imapparser
2 |
3 | import "testing"
4 |
5 | var seqContainsTests = []struct {
6 | seqs []SeqRange
7 | want []uint32
8 | wantNot []uint32
9 | }{
10 | {
11 | seqs: []SeqRange{SeqRange{0, 0}},
12 | want: []uint32{1, 2, 3, 4},
13 | },
14 | {
15 | seqs: []SeqRange{SeqRange{1, 1}, SeqRange{3, 4}},
16 | want: []uint32{1, 3, 4},
17 | wantNot: []uint32{2, 5},
18 | },
19 | {
20 | seqs: []SeqRange{SeqRange{4, 0}},
21 | want: []uint32{4, 5, 6},
22 | wantNot: []uint32{1, 2, 3},
23 | },
24 | }
25 |
26 | func TestSeqContains(t *testing.T) {
27 | for _, test := range seqContainsTests {
28 | for _, id := range test.want {
29 | if !SeqContains(test.seqs, id) {
30 | t.Errorf("SeqContains(%v, %d)=false, want true", test.seqs, id)
31 | }
32 | }
33 | for _, id := range test.wantNot {
34 | if SeqContains(test.seqs, id) {
35 | t.Errorf("SeqContains(%v, %d)=true, want false", test.seqs, id)
36 | }
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/imap/imapparser/types.go:
--------------------------------------------------------------------------------
1 | // Package imapparser implements an IMAP command parser.
2 | //
3 | // It parses client commands for a server.
4 | // At its core it implements the grammar from RFC 3501, along with
5 | // the grammar for several extensions.
6 | //
7 | // See RFC 4466 for the grammar for many typical IMAP extensions.
8 | package imapparser
9 |
10 | import (
11 | "time"
12 |
13 | "crawshaw.io/iox"
14 | )
15 |
16 | type Command struct {
17 | Tag []byte
18 | Name string
19 |
20 | // UID means the command response will report UIDs instead of SeqNums.
21 | // Name is one of: COPY, FETCH, SEARCH, STORE.
22 | UID bool
23 |
24 | // Name is one of:
25 | // SELECT, EXAMINE, SUBSCRIBE, UNSUBSCRIBE, DELETE,
26 | // STATUS, APPEND, COPY
27 | Mailbox []byte
28 |
29 | // Name is one of: SELECT, EXAMINE
30 | Condstore bool
31 | Qresync QresyncParam
32 |
33 | // Name is one of: FETCH, STORE, COPY
34 | Sequences []SeqRange
35 |
36 | // Name is one of: APPEND, STORE
37 | Literal *iox.BufferFile
38 |
39 | Rename struct { // Name: RENAME
40 | OldMailbox []byte
41 | NewMailbox []byte
42 | }
43 |
44 | Params [][]byte // Name: ENABLE, ID
45 |
46 | Auth struct { // Name: LOGIN, AUTHENTICATE PLAIN
47 | Username []byte
48 | Password []byte
49 | }
50 |
51 | List List // Name is one of: LIST, LSUB
52 |
53 | Status struct { // Name: STATUS
54 | Items []StatusItem
55 | }
56 |
57 | Append struct { // Name: APPEND
58 | Flags [][]byte
59 | Date []byte
60 | }
61 |
62 | FetchItems []FetchItem // Name: FETCH
63 | ChangedSince int64 // Name: FETCH
64 | Vanished bool // Name: FETCH
65 |
66 | Store Store // Name: STORE
67 |
68 | Search Search // Name: SEARCH
69 |
70 | ApplePushService *ApplePushService // Name: XAPPLEPUSHSERVICE
71 | }
72 |
73 | type List struct {
74 | ReferenceName []byte
75 | MailboxGlob []byte
76 |
77 | // RFC 5258 LIST-EXTENDED fields
78 | SelectOptions []string // SUBSCRIBED, REMOTE, RECURSIVEMATCH, SPECIAL-USE
79 | ReturnOptions []string // SUBSCRIBED, CHILDREN, SPECIAL-USE
80 | }
81 |
82 | type QresyncParam struct {
83 | UIDValidity uint32
84 | ModSeq int64
85 | UIDs []SeqRange
86 | KnownSeqNumMatch []SeqRange
87 | KnownUIDMatch []SeqRange
88 | }
89 |
90 | type Store struct {
91 | Mode StoreMode
92 | Silent bool
93 | Flags [][]byte
94 | UnchangedSince int64
95 | }
96 |
97 | type ApplePushService struct {
98 | Mailboxes []string
99 | Version int
100 | Subtopic string
101 | Device ApplePushDevice
102 | }
103 |
104 | type ApplePushDevice struct {
105 | AccountID string
106 | DeviceToken string // hex-encoded
107 | }
108 |
109 | type StoreMode int
110 |
111 | const (
112 | StoreUnknown StoreMode = iota
113 | StoreAdd // +FLAGS
114 | StoreRemove // -FLAGS
115 | StoreReplace // FLAGS
116 | )
117 |
118 | type StatusItem int
119 |
120 | const (
121 | StatusUnknownItem StatusItem = iota
122 | StatusMessages
123 | StatusRecent
124 | StatusUIDNext
125 | StatusUIDValidity
126 | StatusUnseen
127 | StatusHighestModSeq
128 | )
129 |
130 | // SeqRange is a normalized IMAP seq-range.
131 | // Normalized means that Min is always less than or equal to Max.
132 | //
133 | // The value 0 is a placeholder for '*'.
134 | // When Min == Max, a SeqRange refers to a single value.
135 | type SeqRange struct {
136 | Min uint32
137 | Max uint32
138 | }
139 |
140 | type FetchItem struct {
141 | Type FetchItemType
142 | Peek bool // BODY.PEEK
143 | Section FetchItemSection // Type is FetchBody
144 | Partial struct {
145 | Start uint32
146 | Length uint32
147 | }
148 | }
149 |
150 | type FetchItemSection struct {
151 | Path []uint16
152 | Name string // One of: HEADER, HEADER.FIELDS[.NOT], TEXT, MIME
153 | Headers [][]byte
154 | }
155 |
156 | type FetchItemType string
157 |
158 | const (
159 | FetchUnknown = FetchItemType("FetchUnknown")
160 |
161 | FetchAll = FetchItemType("ALL") // macro items, only fetch item in list
162 | FetchFull = FetchItemType("FULL")
163 | FetchFast = FetchItemType("FAST")
164 |
165 | FetchEnvelope = FetchItemType("ENVELOPE")
166 | FetchFlags = FetchItemType("FLAGS")
167 | FetchInternalDate = FetchItemType("INTERNALDATE")
168 | FetchRFC822Header = FetchItemType("RFC822.HEADER")
169 | FetchRFC822Size = FetchItemType("RFC822.SIZE")
170 | FetchRFC822Text = FetchItemType("RFC822.TEXT")
171 | FetchUID = FetchItemType("UID")
172 | FetchBodyStructure = FetchItemType("BODYSTRUCTURE")
173 | FetchBody = FetchItemType("BODY")
174 | FetchModSeq = FetchItemType("MODSEQ")
175 | )
176 |
177 | type Search struct {
178 | Op *SearchOp
179 | Charset string
180 | Return []string // MIN, MAX, ALL, COUNT
181 | }
182 |
183 | type SearchOp struct {
184 | // Key is an IMAP search key.
185 | //
186 | // Two extra keys are defined that are not found in RFC 3501:
187 | //
188 | // - AND: every element of Children must match
189 | // It is prettier than the grammar '('.
190 | // This allows the entire search command to be a SearchOp.
191 | //
192 | // - SEQSET: the search op is a match against sequence IDs
193 | // This is a name for the implicit grammar.
194 | //
195 | Key SearchKey
196 |
197 | // Children is set when Key is one of: AND, OR, NOT
198 | // For NOT, len(Children) == 1.
199 | Children []SearchOp
200 |
201 | // Value is set when Key is one of:
202 | // BCC, CC, FROM,
203 | // HEADER (": "),
204 | // KEYWORD, SUBJECT, TEXT, TO
205 | Value string
206 |
207 | Num int64 // Key is one of: LARGER (uint32), SMALLER (uint32), MODSEQ
208 | Sequences []SeqRange // Key is one of: SEQSET, UID, UNDRAFT
209 |
210 | Date time.Time // Key is one of: BEFORE, ON, SENTBEFORE, SENTON, SENTSINCE, SINCE
211 | }
212 |
213 | type SearchKey string
214 |
215 | type Mode int
216 |
217 | const (
218 | ModeNonAuth Mode = iota
219 | ModeAuth
220 | ModeSelected
221 | )
222 |
--------------------------------------------------------------------------------
/imap/imapparser/utf7mod/utf7mod.go:
--------------------------------------------------------------------------------
1 | // Package utf7mod implements "Modified UTF-7".
2 | //
3 | // Modified UTF-7 is described in RFC 3501 section 5.1.3,
4 | // based on the original UTF-7 defined in RFC 2152.
5 | //
6 | // There are several MUST requirements in the spec that
7 | // we relax for decoding. There are no good options when
8 | // faced with bad UTF-7, so we make do as best we can.
9 | package utf7mod
10 |
11 | import (
12 | "bytes"
13 | "encoding/base64"
14 | "errors"
15 | "fmt"
16 | "unicode/utf16"
17 | "unicode/utf8"
18 | )
19 |
20 | var ErrInvalidUTF7 = errors.New("utf7mod: invalid UTF-7")
21 |
22 | const encodeModB64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,"
23 |
24 | // Modified UTF-7 uses a modified base64, described as:
25 | //
26 | // modified BASE64, with a further modification from
27 | // [UTF-7] that "," is used instead of "/".
28 | var b64 = base64.NewEncoding(encodeModB64).WithPadding(base64.NoPadding)
29 |
30 | func AppendDecode(dst, src []byte) ([]byte, error) {
31 | for len(src) > 0 {
32 | c := src[0]
33 | src = src[1:]
34 | if c != '&' {
35 | dst = append(dst, c)
36 | continue
37 | }
38 | i := bytes.IndexByte(src, '-')
39 | if i == -1 {
40 | return nil, ErrInvalidUTF7
41 | }
42 | if i == 0 {
43 | src = src[1:]
44 | dst = append(dst, '&')
45 | continue
46 | }
47 | scratch := make([]byte, 0, 64)
48 | scratch = append(scratch, make([]byte, b64.DecodedLen(i))...)
49 | n, err := b64.Decode(scratch, src[:i])
50 | src = src[i+1:]
51 | if err != nil {
52 | return nil, fmt.Errorf("utf7mod: decode: %v", err)
53 | }
54 | scratch = scratch[:n]
55 | if len(scratch)%1 == 1 {
56 | return nil, ErrInvalidUTF7
57 | }
58 | for len(scratch) > 0 {
59 | r := rune(scratch[0])<<8 | rune(scratch[1])
60 | scratch = scratch[2:]
61 | if utf16.IsSurrogate(r) {
62 | if len(scratch) == 0 {
63 | return nil, ErrInvalidUTF7
64 | }
65 | r2 := rune(scratch[0])<<8 | rune(scratch[1])
66 | scratch = scratch[2:]
67 | r = utf16.DecodeRune(r, r2)
68 | }
69 | dst = appendRune(dst, r)
70 | }
71 | }
72 | return dst, nil
73 | }
74 |
75 | func appendRune(slice []byte, c rune) []byte {
76 | var b [4]byte
77 | return append(slice, b[:utf8.EncodeRune(b[:], c)]...)
78 | }
79 |
80 | func AppendEncode(dst, src []byte) ([]byte, error) {
81 | for len(src) > 0 {
82 | r, _ := utf8.DecodeRune(src)
83 | if r == '&' {
84 | dst = append(dst, '&', '-')
85 | src = src[1:]
86 | continue
87 | } else if r < utf8.RuneSelf {
88 | dst = append(dst, byte(r))
89 | src = src[1:]
90 | continue
91 | }
92 | // Encode a sequence of non-ASCII as base64-encoded utf16be.
93 | scratch := make([]byte, 0, 64)
94 | for len(src) > 0 {
95 | r, sz := utf8.DecodeRune(src)
96 | if r < utf8.RuneSelf {
97 | break
98 | }
99 | src = src[sz:]
100 | if r1, r2 := utf16.EncodeRune(r); r1 != '\uFFFD' {
101 | scratch = append(scratch, byte(r1>>8), byte(r1))
102 | r = r2
103 | }
104 | scratch = append(scratch, byte(r>>8), byte(r))
105 | }
106 |
107 | // Pad the UTF-16BE with zeros as per RFC2152.
108 | b64len := b64.EncodedLen(len(scratch))
109 |
110 | dst = append(dst, '&')
111 | dst = append(dst, make([]byte, b64len)...)
112 | b64.Encode(dst[len(dst)-b64len:], scratch)
113 | dst = append(dst, '-')
114 | }
115 |
116 | return dst, nil
117 | }
118 |
--------------------------------------------------------------------------------
/imap/imapparser/utf7mod/utf7mod_test.go:
--------------------------------------------------------------------------------
1 | package utf7mod
2 |
3 | import "testing"
4 |
5 | var tests = []struct {
6 | dec, enc string
7 | errstr string
8 | }{
9 | {dec: "&", enc: "&-"},
10 | {dec: "&&", enc: "&-&-"},
11 | {dec: "Hello, 世界", enc: "Hello, &ThZ1TA-"},
12 | {dec: "🤓", enc: "&2D7dEw-"},
13 | {dec: "~peter/mail/台北/日本語", enc: "~peter/mail/&U,BTFw-/&ZeVnLIqe-"},
14 | }
15 |
16 | func TestAppendEncode(t *testing.T) {
17 | for _, test := range tests {
18 | t.Run(test.dec, func(t *testing.T) {
19 | enc, err := AppendEncode(nil, []byte(test.dec))
20 | if err != nil {
21 | t.Fatal(err)
22 | }
23 | if got := string(enc); got != test.enc {
24 | t.Errorf("encode %q=%q, want %q", test.dec, got, test.enc)
25 | }
26 | })
27 | }
28 | }
29 |
30 | func TestAppendDecode(t *testing.T) {
31 | for _, test := range tests {
32 | t.Run(test.dec, func(t *testing.T) {
33 | dec, err := AppendDecode(nil, []byte(test.enc))
34 | if err != nil {
35 | t.Fatal(err)
36 | }
37 | if got := string(dec); got != test.dec {
38 | t.Errorf("encode %q=%q, want %q", test.enc, got, test.dec)
39 | }
40 | })
41 | }
42 | }
43 |
44 | func BenchmarkEncodeAlloc(b *testing.B) {
45 | dst := make([]byte, 0, 1024)
46 |
47 | var inputs [][]byte
48 | for _, test := range tests {
49 | if test.errstr != "" {
50 | continue
51 | }
52 | inputs = append(inputs, []byte(test.dec))
53 | }
54 |
55 | b.ReportAllocs()
56 | for i := 0; i < b.N; i++ {
57 | for _, input := range inputs {
58 | _, err := AppendEncode(dst, input)
59 | if err != nil {
60 | b.Fatal(err)
61 | }
62 | }
63 | }
64 | }
65 |
66 | func BenchmarkDecodeAlloc(b *testing.B) {
67 | dst := make([]byte, 0, 1024)
68 |
69 | var inputs [][]byte
70 | for _, test := range tests {
71 | if test.errstr != "" {
72 | continue
73 | }
74 | inputs = append(inputs, []byte(test.enc))
75 | }
76 |
77 | b.ReportAllocs()
78 | for i := 0; i < b.N; i++ {
79 | for _, input := range inputs {
80 | _, err := AppendDecode(dst, input)
81 | if err != nil {
82 | b.Fatal(err)
83 | }
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/imap/imapserver/apns.go:
--------------------------------------------------------------------------------
1 | package imapserver
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "crypto/tls"
7 | "crypto/x509"
8 | "encoding/hex"
9 | "encoding/json"
10 | "errors"
11 | "log"
12 | "time"
13 |
14 | "spilled.ink/imap/imapparser"
15 | )
16 |
17 | // APNS sends message notifications to the Apple Push Notification Service.
18 | //
19 | // To send notifications you need a certificate from Apple.
20 | // It can be generated as a .p12 file from the old Mac Server App.
21 | // Then it can be converted to cert/key files with:
22 | //
23 | // openssl pkcs12 -in apns.mail.p12 -out apns.crt.pem -clcerts -nokeys
24 | // openssl pkcs12 -in apns.mail.p12 -out apns.key.pem -nocerts -nodes
25 | type APNS struct {
26 | Certificate tls.Certificate // create with tls.LoadX509KeyPair
27 | GatewayAddr string // default value: gateway.push.apple.com
28 | UID string // default value extracted from Certificate
29 |
30 | ctx context.Context
31 | ctxCancel func()
32 | shutdownComplete chan struct{}
33 | notify chan imapparser.ApplePushDevice
34 | }
35 |
36 | // http://www.alvestrand.no/objectid/0.9.2342.19200300.100.1.1.html
37 | var oidUserID = []int{0, 9, 2342, 19200300, 100, 1, 1}
38 |
39 | func (a *APNS) start() error {
40 | if a.GatewayAddr == "" {
41 | a.GatewayAddr = "gateway.push.apple.com:2195"
42 | }
43 | if a.UID == "" {
44 | leafCert, err := x509.ParseCertificate(a.Certificate.Certificate[0])
45 | if err != nil {
46 | panic(err)
47 | }
48 |
49 | for _, n := range leafCert.Subject.Names {
50 | if n.Type.Equal(oidUserID) {
51 | if v, ok := n.Value.(string); ok {
52 | a.UID = v
53 | break
54 | }
55 | }
56 | }
57 | if a.UID == "" {
58 | return errors.New("APNS: certificate has no UID")
59 | }
60 | }
61 |
62 | a.ctx, a.ctxCancel = context.WithCancel(context.Background())
63 | a.shutdownComplete = make(chan struct{})
64 | a.notify = make(chan imapparser.ApplePushDevice, 32)
65 | go a.sender()
66 | return nil
67 | }
68 |
69 | func (a *APNS) shutdown() {
70 | a.ctxCancel()
71 | <-a.shutdownComplete
72 | }
73 |
74 | func (a *APNS) Notify(devices []imapparser.ApplePushDevice) {
75 | for _, device := range devices {
76 | select {
77 | case a.notify <- device:
78 | case <-a.ctx.Done():
79 | }
80 | }
81 | }
82 |
83 | func (a *APNS) sender() {
84 | for {
85 | select {
86 | case <-a.ctx.Done():
87 | close(a.shutdownComplete)
88 | return
89 | case device := <-a.notify:
90 | a.send(device)
91 | }
92 | }
93 | }
94 |
95 | func (a *APNS) send(device imapparser.ApplePushDevice) {
96 | config := &tls.Config{}
97 | if a.Certificate.Certificate != nil {
98 | config.Certificates = []tls.Certificate{a.Certificate}
99 | }
100 | c, err := tls.Dial("tcp", a.GatewayAddr, config)
101 | if err != nil {
102 | log.Printf("APNS: %v", err) // TODO better logging
103 | return
104 | }
105 | defer c.Close()
106 |
107 | buf := new(bytes.Buffer)
108 | for {
109 | buf.Reset()
110 | buf.WriteByte(0)
111 | buf.WriteByte(0)
112 | buf.WriteByte(0x20)
113 |
114 | token, err := hex.DecodeString(device.DeviceToken)
115 | if err != nil {
116 | log.Printf("APNS: bad token: %v: %v", device, err)
117 | continue
118 | }
119 | buf.Write(token)
120 | buf.WriteByte(0)
121 |
122 | data := map[string]interface{}{
123 | "aps": map[string]interface{}{
124 | "account-id": device.AccountID,
125 | },
126 | }
127 | jsonText, err := json.Marshal(data)
128 | if err != nil {
129 | panic("APNS: bad JSON: " + err.Error())
130 | }
131 | if len(jsonText) > 1<<8-1 {
132 | log.Printf("APNS: JSON too big: %d", len(jsonText))
133 | continue
134 | }
135 | buf.WriteByte(byte(len(jsonText)))
136 | buf.Write(jsonText)
137 |
138 | if _, err := buf.WriteTo(c); err != nil {
139 | log.Printf("APNS: failed to write: %v", err)
140 | // Slow down. Don't overwhelm the gateway on error.
141 | time.Sleep(1 * time.Second)
142 | return
143 | }
144 | log.Printf("APNS push notification sent for %v", device)
145 |
146 | select {
147 | case device = <-a.notify:
148 | // loop with new device
149 | case <-a.ctx.Done():
150 | return
151 | case <-time.After(5 * time.Second):
152 | return
153 | }
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/imap/imapserver/debug.go:
--------------------------------------------------------------------------------
1 | package imapserver
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io"
7 | "sync"
8 | "time"
9 | )
10 |
11 | const debugLiteralWrite = 256 // number of bytes of the literal to write
12 |
13 | // debugWriter writes a copy of an IMAP session.
14 | // It skips over long literals.
15 | //
16 | // There is no buffering in debugWriter because the imapserver
17 | // batches writes to it using the same bufio it uses to batch
18 | // network communication.
19 | type debugWriter struct {
20 | sessionID string
21 | logf func(format string, v ...interface{}) // used to report failed writing
22 |
23 | mu sync.Mutex
24 | writer io.Writer
25 | client *debugWriterDirectional
26 | server *debugWriterDirectional
27 | lastPrefix string
28 | }
29 |
30 | func newDebugWriter(sessionID string, logf func(format string, v ...interface{}), writer io.Writer) *debugWriter {
31 | w := &debugWriter{
32 | sessionID: sessionID,
33 | logf: logf,
34 | writer: writer,
35 | }
36 | w.client = &debugWriterDirectional{
37 | w: w,
38 | prefix: "C: ",
39 | }
40 | w.server = &debugWriterDirectional{
41 | w: w,
42 | prefix: "S: ",
43 | }
44 | return w
45 | }
46 |
47 | type debugWriterDirectional struct {
48 | w *debugWriter
49 | prefix string
50 | litHead int
51 | litSkip int
52 | }
53 |
54 | func (w *debugWriterDirectional) literalDataFollows(n int) {
55 | w.w.mu.Lock()
56 | defer w.w.mu.Unlock()
57 |
58 | if n < debugLiteralWrite {
59 | return // write the whole literal
60 | }
61 | w.litHead = debugLiteralWrite / 2
62 | litTail := debugLiteralWrite / 2
63 | w.litSkip = n - w.litHead - litTail
64 | }
65 |
66 | func (w *debugWriterDirectional) Write(p []byte) (int, error) {
67 | w.w.mu.Lock()
68 | defer w.w.mu.Unlock()
69 |
70 | n := len(p)
71 |
72 | if w.litHead > 0 {
73 | head := p
74 | if len(head) > w.litHead {
75 | head = head[:w.litHead]
76 | }
77 | // TODO: prefix write head
78 | if !w.writeWithPrefix(head) {
79 | return n, nil
80 | }
81 | w.litHead -= len(head)
82 | p = p[len(head):]
83 | if w.litHead == 0 {
84 | fmt.Fprintf(w.w.writer, "\n%s... skipping %d bytes of literal ...\n", w.prefix, w.litSkip)
85 | w.w.lastPrefix = ""
86 | }
87 | }
88 | if w.litSkip > 0 {
89 | if len(p) < w.litSkip {
90 | w.litSkip -= len(p)
91 | return n, nil
92 | }
93 | p = p[w.litSkip:]
94 | w.litSkip = 0
95 | }
96 |
97 | w.writeWithPrefix(p)
98 | return n, nil
99 | }
100 |
101 | func (w *debugWriterDirectional) writeWithPrefix(p []byte) bool {
102 | if len(p) == 0 {
103 | return true
104 | }
105 | if w.w.lastPrefix != w.prefix {
106 | if !w.writePrefix() {
107 | return false
108 | }
109 | }
110 | for len(p) > 0 {
111 | i := bytes.IndexByte(p, '\n')
112 | if i == -1 {
113 | break
114 | }
115 | if !w.write(p[:i+1]) {
116 | return false
117 | }
118 | p = p[i+1:]
119 | if len(p) == 0 {
120 | w.w.lastPrefix = "" // whoever comes next should write a prefix
121 | break
122 | }
123 | if !w.writePrefix() {
124 | return false
125 | }
126 | }
127 | if !w.write(p) {
128 | return false
129 | }
130 | return true
131 | }
132 |
133 | func (w *debugWriterDirectional) write(p []byte) bool {
134 | if _, err := w.w.writer.Write(p); err != nil {
135 | w.w.logf("session(%s): debugWriter failed: %v", w.w.sessionID, err)
136 | return false
137 | }
138 | return true
139 | }
140 |
141 | func (w *debugWriterDirectional) writePrefix() bool {
142 | w.w.lastPrefix = w.prefix
143 | b := make([]byte, 0, 32)
144 | b = time.Now().AppendFormat(b, "15:04:05.000 ")
145 | b = append(b, w.prefix...)
146 | if _, err := w.w.writer.Write(b); err != nil {
147 | w.w.logf("session(%s): debugWriter failed: %v", w.w.sessionID, err)
148 | return false
149 | }
150 | return true
151 | }
152 |
--------------------------------------------------------------------------------
/imap/imapserver/imapserver_test.go:
--------------------------------------------------------------------------------
1 | package imapserver_test
2 |
3 | import (
4 | "context"
5 | "testing"
6 | "time"
7 |
8 | "crawshaw.io/iox"
9 | "spilled.ink/imap/imaptest"
10 | )
11 |
12 | func Test(t *testing.T) {
13 | filer := iox.NewFiler(0)
14 | filer.DefaultBufferMemSize = 1 << 20
15 | filer.Logf = t.Logf
16 | defer func() {
17 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
18 | defer cancel()
19 | filer.Shutdown(ctx)
20 | }()
21 |
22 | t.Run("Memory", func(t *testing.T) {
23 | for _, test := range imaptest.Tests {
24 | test := test
25 | t.Run(test.Name, func(t *testing.T) {
26 | t.Parallel()
27 | dataStore := &imaptest.MemoryStore{
28 | Filer: filer,
29 | }
30 | server, err := imaptest.InitTestServer(filer, dataStore, dataStore)
31 | if err != nil {
32 | t.Fatal(err)
33 | }
34 | defer func() {
35 | dataStore.Close()
36 | if err := server.Shutdown(); err != nil {
37 | t.Fatal(err)
38 | }
39 | }()
40 |
41 | test.Fn(t, server)
42 | })
43 | }
44 | })
45 | }
46 |
--------------------------------------------------------------------------------
/imap/imapserver/log.go:
--------------------------------------------------------------------------------
1 | package imapserver
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | "time"
7 |
8 | "spilled.ink/email"
9 | )
10 |
11 | type logMsg struct {
12 | What string
13 | When time.Time
14 | Duration time.Duration
15 | ID string
16 | UserID int64
17 | MsgID email.MsgID
18 | PartNum int
19 | Err error
20 | Data string
21 | }
22 |
23 | func (l logMsg) String() string {
24 | const where = "imap"
25 |
26 | buf := new(strings.Builder)
27 | fmt.Fprintf(buf, `{"where": %q, "what": %q, `, where, l.What)
28 |
29 | if l.When.IsZero() {
30 | l.When = time.Now()
31 | }
32 | buf.WriteString(`"when": "`)
33 | buf.Write(l.When.AppendFormat(make([]byte, 0, 64), time.RFC3339Nano))
34 | buf.WriteString(`"`)
35 |
36 | if l.Duration != 0 {
37 | fmt.Fprintf(buf, `, "duration": "%s"`, l.Duration)
38 | }
39 | if l.ID != "" {
40 | fmt.Fprintf(buf, `, "session_id": "%s"`, l.ID)
41 | }
42 | if l.UserID != 0 {
43 | fmt.Fprintf(buf, `, "user_id": "%d"`, l.UserID)
44 | }
45 | if l.MsgID != 0 {
46 | fmt.Fprintf(buf, `, "msg_id": "%d"`, l.MsgID)
47 | }
48 | if l.PartNum != 0 {
49 | fmt.Fprintf(buf, `, "part_num": "%d"`, l.PartNum)
50 | }
51 | if l.Err != nil {
52 | fmt.Fprintf(buf, `, "err": %q`, l.Err.Error())
53 | }
54 | if l.Data != "" {
55 | fmt.Fprintf(buf, `, "data": "%s"`, l.Data)
56 | }
57 | buf.WriteByte('}')
58 | return buf.String()
59 | }
60 |
--------------------------------------------------------------------------------
/imap/imapserver/toyserver.go:
--------------------------------------------------------------------------------
1 | //+build ignore
2 |
3 | package main
4 |
5 | import (
6 | "log"
7 | "net"
8 |
9 | "crawshaw.io/iox"
10 | "spilled.ink/imap/imapserver"
11 | "spilled.ink/util/tlstest"
12 | )
13 |
14 | func main() {
15 | s := &imapserver.Server{
16 | TLSConfig: tlstest.ServerConfig,
17 | Filer: iox.NewFiler(0),
18 | Logf: log.Printf,
19 | }
20 |
21 | ln, err := net.Listen("tcp", "localhost:8993")
22 | if err != nil {
23 | panic(err)
24 | }
25 | log.Printf("serving IMAP on %s", ln.Addr())
26 | if err := s.ServeTLS(ln); err != nil {
27 | panic(err)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/smtp/smtpclient/smtpclient.go:
--------------------------------------------------------------------------------
1 | package smtpclient
2 |
3 | import (
4 | "context"
5 | "crypto/tls"
6 | "io"
7 | "net"
8 | "net/smtp"
9 | "net/textproto"
10 | "strings"
11 | "time"
12 | )
13 |
14 | type Client struct {
15 | LocalHostname string // name of this host
16 | LocalAddr net.Addr // address on this host to send from
17 | Resolver *net.Resolver
18 |
19 | limiter chan struct{} // per open connection
20 | }
21 |
22 | func NewClient(localHostname string, maxConcurrent int) *Client {
23 | return &Client{
24 | Resolver: net.DefaultResolver,
25 | LocalHostname: localHostname,
26 | limiter: make(chan struct{}, maxConcurrent),
27 | }
28 | }
29 |
30 | type Delivery struct {
31 | Recipient string
32 | Code int
33 | Details string
34 | Date time.Time
35 | Error error
36 | }
37 |
38 | func (d Delivery) Success() bool { return d.Code == 250 && d.Error == nil }
39 | func (d Delivery) PermFailure() bool { return d.Code >= 500 }
40 | func (d Delivery) TempFailure() bool { return (d.Code >= 400 && d.Code < 500) || d.Error != nil }
41 |
42 | func (c *Client) Send(ctx context.Context, from string, recipients []string, contents io.ReaderAt, contentSize int64) (results []Delivery, err error) {
43 | mxDomain := make(map[string]string) // domain name -> MX record (a local lookup cache)
44 | spools := make(map[string][]string) // MX spool -> recipients
45 |
46 | for _, to := range recipients {
47 | domain := to[strings.LastIndexByte(to, '@')+1:]
48 | mxAddr := mxDomain[domain]
49 | if mxAddr != "" {
50 | spools[mxAddr] = append(spools[mxAddr], to)
51 | continue
52 | }
53 | mxs, err := c.Resolver.LookupMX(ctx, domain)
54 | if err != nil {
55 | continue
56 | }
57 | pref := uint16(50000)
58 | for _, opt := range mxs {
59 | if opt.Pref < pref {
60 | mxAddr = opt.Host
61 | pref = opt.Pref
62 | }
63 | }
64 | if mxAddr == "" {
65 | continue
66 | }
67 |
68 | mxDomain[domain] = mxAddr
69 | spools[mxAddr] = append(spools[mxAddr], to)
70 | }
71 |
72 | select {
73 | case <-ctx.Done():
74 | return nil, context.Canceled
75 | default:
76 | }
77 |
78 | deliveries := 0
79 | for _, rcpts := range spools {
80 | deliveries += len(rcpts)
81 | }
82 |
83 | resultsCh := make(chan Delivery, deliveries)
84 | go func() {
85 | for mxAddr, rcpts := range spools {
86 | r := io.NewSectionReader(contents, 0, contentSize)
87 | results := c.send(ctx, mxAddr+":25", from, rcpts, r)
88 | for _, res := range results {
89 | resultsCh <- res
90 | }
91 | }
92 | }()
93 |
94 | results = make([]Delivery, deliveries)
95 | for i := range results {
96 | results[i] = <-resultsCh
97 | }
98 | return results, nil
99 | }
100 |
101 | func (c *Client) send(ctx context.Context, mxAddr string, from string, recipients []string, r io.Reader) (results []Delivery) {
102 | results = make([]Delivery, len(recipients))
103 | for i, rcpt := range recipients {
104 | results[i].Recipient = rcpt
105 | }
106 | allErr := func(err error) []Delivery {
107 | for i := range results {
108 | if results[i].Code == 0 {
109 | results[i].Error = err
110 | }
111 | }
112 | return results
113 | }
114 |
115 | select {
116 | case c.limiter <- struct{}{}:
117 | case <-ctx.Done():
118 | return allErr(context.Canceled)
119 | }
120 | defer func() { <-c.limiter }()
121 |
122 | dialer := &net.Dialer{
123 | Resolver: c.Resolver,
124 | LocalAddr: c.LocalAddr,
125 | }
126 | tcpConn, err := dialer.DialContext(ctx, "tcp", mxAddr)
127 | if err != nil {
128 | return allErr(err)
129 | }
130 | host, _, _ := net.SplitHostPort(mxAddr)
131 | mxConn, err := smtp.NewClient(tcpConn, host)
132 | if err != nil {
133 | return allErr(err)
134 | }
135 |
136 | done := make(chan struct{})
137 | go func() {
138 | select {
139 | case <-ctx.Done():
140 | case <-done:
141 | }
142 | mxConn.Close()
143 | }()
144 | defer func() { close(done) }()
145 |
146 | tlsConfig := &tls.Config{
147 | // TODO: do better for servers we know we can trust:
148 | // https://starttls-everywhere.org/
149 | InsecureSkipVerify: true,
150 | }
151 | if err := mxConn.Hello(c.LocalHostname); err != nil {
152 | return allErr(err)
153 | }
154 | if err := mxConn.StartTLS(tlsConfig); err != nil {
155 | return allErr(err)
156 | }
157 | if err := mxConn.Mail(from); err != nil {
158 | return allErr(err)
159 | }
160 | deliverAttempt := 0
161 | for i, to := range recipients {
162 | if rcptErr := mxConn.Rcpt(to); rcptErr != nil {
163 | if tperr, _ := rcptErr.(*textproto.Error); tperr != nil {
164 | results[i].Code = tperr.Code
165 | results[i].Details = tperr.Msg
166 | continue
167 | }
168 | err = rcptErr
169 | break
170 | }
171 | deliverAttempt++
172 | }
173 | if err != nil {
174 | return allErr(err)
175 | }
176 | if deliverAttempt == 0 {
177 | return results
178 | }
179 |
180 | w, err := mxConn.Data()
181 | if err != nil {
182 | return allErr(err)
183 | }
184 | if _, err := io.Copy(w, r); err != nil {
185 | return allErr(err)
186 | }
187 | if err := w.Close(); err != nil {
188 | return allErr(err)
189 | }
190 | if err := mxConn.Quit(); err != nil {
191 | return allErr(err)
192 | }
193 | for i := range results {
194 | if results[i].Code == 0 && results[i].Error == nil {
195 | results[i].Code = 250
196 | }
197 | }
198 | return results
199 | }
200 |
--------------------------------------------------------------------------------
/smtp/smtpserver/greylist/greylist.go:
--------------------------------------------------------------------------------
1 | // Package greylist implements a variant of SMTP greylisting.
2 | //
3 | // A popular implementation of greylisting is OpenBSD's spamd(8).
4 | // Details of how it works are available in its man page:
5 | // http://man.openbsd.org/spamd.
6 | //
7 | // More general details of the algorithm are available at
8 | // https://www.greylisting.org/.
9 | //
10 | // This implementation is relatively heavy-weight.
11 | // Instead of tarpitting the first greylist attempt,
12 | // the message is analyzed to see if there are other signals
13 | // that should allow the first message to pass through.
14 | package greylist
15 |
16 | import (
17 | "context"
18 | "errors"
19 | "io"
20 | "net"
21 | "time"
22 |
23 | "crawshaw.io/iox"
24 |
25 | "spilled.ink/smtp/smtpserver"
26 | )
27 |
28 | var ErrNotFound = errors.New("greylist: IP-from-to tuple not found")
29 |
30 | type DB interface {
31 | Get(ctx context.Context, remoteAddr, from, to string) (time.Time, error)
32 | Put(ctx context.Context, remoteAddr, from, to string) error
33 | }
34 |
35 | // Greylist provides an smtpserver.NewMessageFunc that implements greylisting.
36 | //
37 | // If the message passes analysis then ProcessMsg is called.
38 | type Greylist struct {
39 | Filer *iox.Filer
40 | ProcessMsg func(ctx context.Context, msg *RawMsg) error
41 | Whitelist func(ctx context.Context, remoteAddr net.Addr, from []byte) (bool, error)
42 | Blacklist func(ctx context.Context, remoteAddr net.Addr, from []byte) (bool, error)
43 | GreyDB DB
44 | }
45 |
46 | func (gl *Greylist) NewMessage(ctx context.Context, remoteAddr net.Addr, from []byte, authToken uint64) (smtpserver.Msg, error) {
47 | msg := &greyMsg{
48 | ctx: ctx,
49 | gl: gl,
50 | rawMsg: new(RawMsg),
51 | }
52 | msg.buf = append(msg.buf, from...)
53 | msg.rawMsg.From = msg.buf[0:len(from):len(from)]
54 | msg.rawMsg.RemoteAddr = remoteAddr
55 |
56 | return msg, nil
57 | }
58 |
59 | // TODO: move to email package?
60 | type RawMsg struct {
61 | RemoteAddr net.Addr
62 | From []byte
63 | Recipients [][]byte
64 | Whitelist bool
65 | Content io.ReadCloser
66 | ContentSize int64
67 | // TODO: DKIM analysis, SPF results, etc
68 | }
69 |
70 | type greyMsg struct {
71 | ctx context.Context
72 | gl *Greylist
73 | f *iox.BufferFile
74 | rawMsg *RawMsg
75 | buf []byte
76 | }
77 |
78 | func (g *greyMsg) AddRecipient(addr []byte) (bool, error) {
79 | g.buf = append(g.buf, addr...)
80 | addr = g.buf[len(g.buf)-len(addr) : len(g.buf) : len(g.buf)]
81 | g.rawMsg.Recipients = append(g.rawMsg.Recipients, addr)
82 | return true, nil
83 | }
84 |
85 | func (g *greyMsg) Write(line []byte) error {
86 | if g.f == nil {
87 | g.f = g.gl.Filer.BufferFile(0)
88 | }
89 | n, err := g.f.Write(line)
90 | g.rawMsg.ContentSize += int64(n)
91 | return err
92 | }
93 |
94 | func (g *greyMsg) Cancel() {
95 | if g.f != nil {
96 | g.f.Close()
97 | }
98 | }
99 |
100 | func (g *greyMsg) allow() (bool, error) {
101 | if g.gl.Whitelist != nil {
102 | if is, err := g.gl.Whitelist(g.ctx, g.rawMsg.RemoteAddr, g.rawMsg.From); err != nil {
103 | return false, err
104 | } else if is {
105 | g.rawMsg.Whitelist = true
106 | return true, nil
107 | }
108 | }
109 | if g.gl.Blacklist != nil {
110 | if is, err := g.gl.Blacklist(g.ctx, g.rawMsg.RemoteAddr, g.rawMsg.From); err != nil {
111 | return false, err
112 | } else if is {
113 | return false, nil
114 | }
115 | }
116 | // TODO: g.gl.DB.Get / Put
117 | return true, nil
118 | }
119 |
120 | func (g *greyMsg) Close() error {
121 | defer func() {
122 | if g.f != nil {
123 | g.f.Close()
124 | }
125 | }()
126 |
127 | if _, err := g.f.Seek(0, 0); err != nil {
128 | return err
129 | }
130 | g.rawMsg.Content = g.f
131 |
132 | return g.gl.ProcessMsg(g.ctx, g.rawMsg)
133 | }
134 |
--------------------------------------------------------------------------------
/spilldb/boxmgmt/boxmgmt.go:
--------------------------------------------------------------------------------
1 | // Package boxmgmt manages local user mailboxes.
2 | //
3 | // As a general principle, code should use either the main spilldb
4 | // configuration database or the user's spillbox database.
5 | // The few pieces of code that do need to touch both are isolated
6 | // in this package, if possible.
7 | package boxmgmt
8 |
9 | import (
10 | "context"
11 | "fmt"
12 | "os"
13 | "path/filepath"
14 | "sync"
15 |
16 | "crawshaw.io/iox"
17 | "crawshaw.io/sqlite/sqlitex"
18 | "spilled.ink/imap"
19 | "spilled.ink/spilldb/spillbox"
20 | )
21 |
22 | type BoxMgmt struct {
23 | filer *iox.Filer
24 | spilldPool *sqlitex.Pool
25 | dbdir string
26 |
27 | mu sync.Mutex
28 | users map[int64]*User // userID -> user
29 | notifiers []imap.Notifier
30 | }
31 |
32 | func New(filer *iox.Filer, spilldPool *sqlitex.Pool, dbdir string) (*BoxMgmt, error) {
33 | bm := &BoxMgmt{
34 | filer: filer,
35 | spilldPool: spilldPool,
36 | dbdir: dbdir,
37 | users: make(map[int64]*User),
38 | }
39 | return bm, nil
40 | }
41 |
42 | func (bm *BoxMgmt) RegisterNotifier(n imap.Notifier) {
43 | bm.mu.Lock()
44 | defer bm.mu.Unlock()
45 |
46 | bm.notifiers = append(bm.notifiers, n)
47 | for _, u := range bm.users {
48 | u.Box.RegisterNotifier(n)
49 | }
50 | }
51 |
52 | // Open returns an existing user's database connection.
53 | // It returns a cached connection if the user db is already open.
54 | // TODO: rename. We don't track openness as a resource so the name is confusing.
55 | func (bm *BoxMgmt) Open(ctx context.Context, userID int64) (*User, error) {
56 | bm.mu.Lock()
57 | defer bm.mu.Unlock()
58 |
59 | u := bm.users[userID]
60 | if u != nil {
61 | return u, nil
62 | }
63 | u = &User{
64 | userID: userID,
65 | }
66 |
67 | dbfile := "file::memory:?mode=memory"
68 | if bm.dbdir != "" {
69 | dir := filepath.Join(bm.dbdir, "users")
70 | os.MkdirAll(dir, 0770)
71 | dbfile = filepath.Join(dir, fmt.Sprintf("spilld_user%d.db", userID))
72 | }
73 | box, err := spillbox.New(userID, bm.filer, dbfile, 4)
74 | if err != nil {
75 | return nil, err
76 | }
77 | for _, n := range bm.notifiers {
78 | box.RegisterNotifier(n)
79 | }
80 |
81 | u.Box = box
82 | bm.users[userID] = u
83 | return u, nil
84 | }
85 |
86 | func (bm *BoxMgmt) Close() error {
87 | bm.mu.Lock()
88 | defer bm.mu.Unlock()
89 |
90 | var err error
91 | for _, user := range bm.users {
92 | if uErr := user.Box.Close(); err == nil {
93 | err = uErr
94 | }
95 | }
96 | return err
97 | }
98 |
99 | // TODO: remove and use *spillbox.Box directly?
100 | type User struct {
101 | userID int64
102 | Box *spillbox.Box
103 | }
104 |
105 | func (u *User) UserName() string {
106 | return "crawshaw@spilled.ink" // TODO
107 | }
108 |
--------------------------------------------------------------------------------
/spilldb/db/auth.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "errors"
7 | "fmt"
8 | "time"
9 |
10 | "crawshaw.io/sqlite/sqlitex"
11 |
12 | "golang.org/x/crypto/bcrypt"
13 | "spilled.ink/util/throttle"
14 | )
15 |
16 | type Authenticator struct {
17 | DB *sqlitex.Pool
18 | Throttle throttle.Throttle
19 | Logf func(format string, v ...interface{})
20 | Where string
21 | }
22 |
23 | var errAuthFailed = errors.New("authenticator: internal error")
24 | var errPassDeleted = errors.New("authenticator: password deleted")
25 | var ErrBadCredentials = errors.New("authenticator: bad credentials")
26 |
27 | func (a *Authenticator) AuthDevice(ctx context.Context, remoteAddr, username string, password []byte) (userID int64, err error) {
28 | conn := a.DB.Get(ctx)
29 | if conn == nil {
30 | return 0, context.Canceled
31 | }
32 | defer a.DB.Put(conn)
33 |
34 | start := time.Now()
35 | log := &Log{
36 | Where: a.Where,
37 | What: "auth",
38 | When: start,
39 | Data: map[string]interface{}{
40 | "remote_addr": remoteAddr,
41 | "username": username,
42 | },
43 | }
44 | defer func() {
45 | log.Duration = time.Since(start)
46 | a.Logf("%s", log.String())
47 | }()
48 |
49 | password = bytes.ToUpper(password)
50 | password = bytes.Replace(password, []byte(" "), []byte(""), -1)
51 |
52 | if remoteAddr != "" && a.Throttle.Throttle(remoteAddr) {
53 | log.Data["throttle"] = "remote_addr"
54 | } else if a.Throttle.Throttle(username) {
55 | log.Data["throttle"] = "username"
56 | }
57 | defer func() {
58 | if err != nil {
59 | if remoteAddr != "" {
60 | a.Throttle.Add(remoteAddr)
61 | }
62 | a.Throttle.Add(username)
63 | }
64 | }()
65 |
66 | var devices int
67 | var deviceID int64
68 | stmt := conn.Prep(`SELECT DeviceID, UserID, AppPassHash, Deleted FROM Devices
69 | WHERE UserID IN (SELECT UserID FROM UserAddresses WHERE Address = $username);`)
70 | stmt.SetText("$username", username)
71 | for {
72 | if hasNext, err := stmt.Step(); err != nil {
73 | log.Err = err
74 | return 0, errAuthFailed
75 | } else if !hasNext {
76 | break
77 | }
78 | devices++
79 |
80 | passHash := []byte(stmt.GetText("AppPassHash"))
81 | if err := bcrypt.CompareHashAndPassword(passHash, password); err == nil {
82 | deleted := stmt.GetInt64("Deleted") != 0
83 | deviceID = stmt.GetInt64("DeviceID")
84 | userID = stmt.GetInt64("UserID")
85 | stmt.Reset()
86 |
87 | if deleted {
88 | log.Err = errPassDeleted
89 | return 0, ErrBadCredentials
90 | }
91 | break
92 | }
93 | }
94 | log.Data["device_id"] = deviceID
95 | if devices == 0 {
96 | log.Err = errors.New("unknown username")
97 | return 0, ErrBadCredentials
98 | } else if userID == 0 {
99 | log.Err = errors.New("bad password")
100 | return 0, ErrBadCredentials
101 | }
102 | log.UserID = userID
103 |
104 | stmt = conn.Prep(`UPDATE Devices
105 | SET LastAccessTime = $time, LastAccessAddr = $addr
106 | WHERE DeviceID = $deviceID;`)
107 | stmt.SetInt64("$deviceID", deviceID)
108 | stmt.SetInt64("$time", time.Now().Unix())
109 | stmt.SetText("$addr", remoteAddr)
110 | if _, err := stmt.Step(); err != nil {
111 | log.Err = fmt.Errorf("device update failed: %v", err)
112 | return 0, errAuthFailed
113 | }
114 |
115 | return userID, nil
116 | }
117 |
--------------------------------------------------------------------------------
/spilldb/db/auth_test.go:
--------------------------------------------------------------------------------
1 | package db_test
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "io/ioutil"
7 | "path/filepath"
8 | "strings"
9 | "testing"
10 |
11 | "crawshaw.io/iox"
12 |
13 | "spilled.ink/spilldb/db"
14 | )
15 |
16 | func TestAuthenticator(t *testing.T) {
17 | filer := iox.NewFiler(0)
18 | defer filer.Shutdown(context.Background())
19 |
20 | dir, err := ioutil.TempDir("", "imapdb-test-")
21 | if err != nil {
22 | t.Fatal(err)
23 | }
24 | t.Logf("data store tempdir: %s", dir)
25 | dbpool, err := db.Open(filepath.Join(dir, "spilld.db"))
26 | if err != nil {
27 | t.Fatal(err)
28 | }
29 | defer dbpool.Close()
30 |
31 | conn := dbpool.Get(nil)
32 | const username = "foo@spilled.ink"
33 | const devPassword = "aaaabbbbccccdddd"
34 | userID, err := db.AddUser(conn, db.UserDetails{
35 | EmailAddr: username,
36 | Password: "agenericpassword",
37 | })
38 | pwd := strings.ToUpper(devPassword)
39 | if _, err := db.AddDevice(conn, userID, "testdevice", pwd); err != nil {
40 | t.Fatal(err)
41 | }
42 | dbpool.Put(conn)
43 |
44 | ctx := context.Background()
45 | var log string
46 |
47 | a := &db.Authenticator{
48 | Logf: func(format string, v ...interface{}) {
49 | log = fmt.Sprintf(format, v...)
50 | },
51 | Where: "test",
52 | DB: dbpool,
53 | }
54 | if authUserID, err := a.AuthDevice(ctx, "remote1", username, []byte(pwd)); err != nil {
55 | t.Errorf("AuthDevice failed: %v", err)
56 | } else if userID != authUserID {
57 | t.Errorf("AuthDevice matched userID %d, want %d", authUserID, userID)
58 | }
59 | if log == "" {
60 | t.Error("log missing")
61 | } else if !strings.Contains(log, username) {
62 | t.Errorf("log does not mention username %q", username)
63 | }
64 |
65 | log = ""
66 | if _, err := a.AuthDevice(ctx, "", username, nil); err != db.ErrBadCredentials {
67 | t.Errorf("AuthDevice with bad password want ErrBadCredentials, got %v", err)
68 | } else if !strings.Contains(log, "bad password") {
69 | t.Errorf("AuthDevice with bad password want log to mention it, got %s", log)
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/spilldb/db/db_test.go:
--------------------------------------------------------------------------------
1 | package db_test
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "errors"
7 | "io/ioutil"
8 | "path/filepath"
9 | "reflect"
10 | "strings"
11 | "testing"
12 | "time"
13 |
14 | "crawshaw.io/iox"
15 | "spilled.ink/spilldb/db"
16 | )
17 |
18 | func TestLog(t *testing.T) {
19 | now := time.Now()
20 | l := db.Log{
21 | Where: "here",
22 | What: "it",
23 | When: now,
24 | Duration: 57 * time.Millisecond,
25 | }
26 | data := make(map[string]interface{})
27 | if err := json.Unmarshal([]byte(l.String()), &data); err != nil {
28 | t.Fatal(err)
29 | }
30 | if got, want := data["where"], "here"; got != want {
31 | t.Errorf("where=%q, want %q", got, want)
32 | }
33 | if got, want := data["what"], "it"; got != want {
34 | t.Errorf("where=%q, want %q", got, want)
35 | }
36 | if got, want := data["when"], now.Format(time.RFC3339Nano); got != want {
37 | t.Errorf("when=%q, want %q", got, want)
38 | }
39 | if got, want := data["duration"], "57ms"; got != want {
40 | t.Errorf("duration=%q, want %q", got, want)
41 | }
42 |
43 | l.Err = errors.New("an error msg")
44 | data = make(map[string]interface{})
45 | if err := json.Unmarshal([]byte(l.String()), &data); err != nil {
46 | t.Fatal(err)
47 | }
48 | if got, want := data["err"], l.Err.Error(); got != want {
49 | t.Errorf("err=%q, want %q", got, want)
50 | }
51 |
52 | l.Data = map[string]interface{}{"data1": 42}
53 | data = make(map[string]interface{})
54 | if err := json.Unmarshal([]byte(l.String()), &data); err != nil {
55 | t.Fatal(err)
56 | }
57 | if got, want := data["data"].(map[string]interface{})["data1"], float64(42); got != want {
58 | t.Errorf("data=%f, want %f", got, want)
59 | }
60 | }
61 |
62 | func TestAddUser(t *testing.T) {
63 | filer := iox.NewFiler(0)
64 | defer filer.Shutdown(context.Background())
65 |
66 | dir, err := ioutil.TempDir("", "imapdb-test-")
67 | if err != nil {
68 | t.Fatal(err)
69 | }
70 | t.Logf("data store tempdir: %s", dir)
71 | dbpool, err := db.Open(filepath.Join(dir, "spilld.db"))
72 | if err != nil {
73 | t.Fatal(err)
74 | }
75 | defer dbpool.Close()
76 |
77 | conn := dbpool.Get(nil)
78 | defer dbpool.Put(conn)
79 |
80 | const username = "foo@spilled.ink"
81 | const devPassword = "aaaabbbbccccdddd"
82 | userID, err := db.AddUser(conn, db.UserDetails{
83 | EmailAddr: username,
84 | Password: "agenericpassword",
85 | Admin: true,
86 | })
87 | pwd := strings.ToUpper(devPassword)
88 | if _, err := db.AddDevice(conn, userID, "testdevice", pwd); err != nil {
89 | t.Fatal(err)
90 | }
91 |
92 | if err := db.AddUserAddress(conn, userID, "bar", false); err == nil {
93 | t.Fatal("no error message for adding address without domain")
94 | }
95 | if err := db.AddUserAddress(conn, userID, "bar@spilled.ink", false); err != nil {
96 | t.Fatal(err)
97 | }
98 | if err := db.AddUserAddress(conn, userID, "baz@spilled.ink", false); err != nil {
99 | t.Fatal(err)
100 | }
101 |
102 | wantOtherAddrs := []string{"bar@spilled.ink", "baz@spilled.ink"}
103 | var gotOtherAddrs []string
104 | stmt := conn.Prep("SELECT Address, PrimaryAddr FROM UserAddresses WHERE UserID = $userID ORDER BY Address;")
105 | stmt.SetInt64("$userID", userID)
106 | for {
107 | if hasNext, err := stmt.Step(); err != nil {
108 | t.Fatal(err)
109 | } else if !hasNext {
110 | break
111 | }
112 | if stmt.GetInt64("PrimaryAddr") != 0 {
113 | if got, want := stmt.GetText("Address"), "foo@spilled.ink"; got != want {
114 | t.Errorf("primary addr is %q, want %q", got, want)
115 | }
116 | continue
117 | }
118 | gotOtherAddrs = append(gotOtherAddrs, stmt.GetText("Address"))
119 | }
120 | if !reflect.DeepEqual(wantOtherAddrs, gotOtherAddrs) {
121 | t.Errorf("other addrs: %v, want %v", gotOtherAddrs, wantOtherAddrs)
122 | }
123 |
124 | if err := db.AddUserAddress(conn, userID, "bop@spilled.ink", true); err != nil {
125 | t.Fatal(err)
126 | }
127 |
128 | wantOtherAddrs = []string{"bar@spilled.ink", "baz@spilled.ink", "foo@spilled.ink"}
129 | gotOtherAddrs = []string{}
130 | stmt = conn.Prep("SELECT Address, PrimaryAddr FROM UserAddresses WHERE UserID = $userID ORDER BY Address;")
131 | stmt.SetInt64("$userID", userID)
132 | for {
133 | if hasNext, err := stmt.Step(); err != nil {
134 | t.Fatal(err)
135 | } else if !hasNext {
136 | break
137 | }
138 | if stmt.GetInt64("PrimaryAddr") != 0 {
139 | if got, want := stmt.GetText("Address"), "bop@spilled.ink"; got != want {
140 | t.Errorf("primary addr is %q, want %q", got, want)
141 | }
142 | continue
143 | }
144 | gotOtherAddrs = append(gotOtherAddrs, stmt.GetText("Address"))
145 | }
146 | if !reflect.DeepEqual(wantOtherAddrs, gotOtherAddrs) {
147 | t.Errorf("other addrs: %v, want %v", gotOtherAddrs, wantOtherAddrs)
148 | }
149 |
150 | if err := db.AddUserAddress(conn, userID, "bop@spilled.ink", false); err == nil {
151 | t.Fatal("succeeded in reusing email address")
152 | } else {
153 | if userErr, _ := err.(*db.UserError); userErr == nil {
154 | t.Fatal("reused email address error is not a user error")
155 | } else if !strings.Contains(userErr.UserMsg, "already assigned") {
156 | t.Fatalf(`user error message does not mention "already assigned": %q`, userErr.UserMsg)
157 | }
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/spilldb/db/janitor.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "context"
5 | "time"
6 |
7 | "crawshaw.io/sqlite/sqlitex"
8 | )
9 |
10 | // Janitor does periodic cleaning of the primary spilldb database.
11 | type Janitor struct {
12 | Logf func(format string, v ...interface{})
13 |
14 | ctx context.Context
15 | cancelFn func()
16 | done chan struct{}
17 |
18 | pool *sqlitex.Pool
19 | cleanNow chan struct{}
20 | }
21 |
22 | func NewJanitor(pool *sqlitex.Pool) *Janitor {
23 | ctx, cancelFn := context.WithCancel(context.Background())
24 | j := &Janitor{
25 | Logf: func(format string, v ...interface{}) {},
26 | ctx: ctx,
27 | cancelFn: cancelFn,
28 | done: make(chan struct{}),
29 | pool: pool,
30 | cleanNow: make(chan struct{}),
31 | }
32 |
33 | return j
34 | }
35 |
36 | func (j *Janitor) CleanNow() {
37 | select {
38 | case j.cleanNow <- struct{}{}:
39 | default:
40 | }
41 | }
42 |
43 | func (j *Janitor) Run() error {
44 | defer func() { close(j.done) }()
45 |
46 | t := time.NewTicker(30 * time.Minute)
47 | defer t.Stop()
48 | for {
49 | select {
50 | case <-j.ctx.Done():
51 | return nil
52 | case <-t.C:
53 | case <-j.cleanNow:
54 | }
55 |
56 | if err := j.clean(); err != nil {
57 | if err == context.Canceled {
58 | return nil
59 | }
60 | return nil
61 | }
62 | }
63 | }
64 |
65 | func (j *Janitor) Shutdown(ctx context.Context) error {
66 | j.cancelFn()
67 | <-j.done
68 | return nil
69 | }
70 |
71 | func (j *Janitor) clean() error {
72 | start := time.Now()
73 |
74 | conn := j.pool.Get(j.ctx)
75 | if conn == nil {
76 | return context.Canceled
77 | }
78 | defer j.pool.Put(conn)
79 |
80 | var msgsRemoved int
81 | defer func() {
82 | l := Log{
83 | What: "cleanup",
84 | Where: "janitor",
85 | When: start,
86 | Duration: time.Since(start),
87 | Data: map[string]interface{}{
88 | "msgs_removed": msgsRemoved,
89 | },
90 | }
91 | j.Logf("%s", l)
92 | }()
93 |
94 | return nil
95 | }
96 |
--------------------------------------------------------------------------------
/spilldb/db/sql.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | const createSQL = `
4 | PRAGMA auto_vacuum = INCREMENTAL;
5 |
6 | -- ServerConfig is a one-row table containing global spilld configuration.
7 | CREATE TABLE IF NOT EXISTS ServerConfig (
8 | NexusToken TEXT
9 | -- TODO: consider replicating flags here and using github.com/peterbourgon/ff
10 | );
11 |
12 | CREATE TABLE IF NOT EXISTS Users (
13 | UserID INTEGER PRIMARY KEY,
14 | PassHash TEXT NOT NULL, -- bcrypt of used password
15 | SecretBoxKey TEXT NOT NULL, -- hex encoded 32-byte key
16 | FullName TEXT NOT NULL,
17 | PhoneNumber TEXT NOT NULL,
18 | PhoneVerified BOOLEAN NOT NULL,
19 | Admin BOOLEAN NOT NULL,
20 | Locked BOOLEAN NOT NULL
21 | );
22 |
23 | CREATE TABLE IF NOT EXISTS UserAddresses (
24 | Address TEXT PRIMARY KEY, -- "user@domain", always lower case
25 | UserID INTEGER NOT NULL,
26 | PrimaryAddr BOOLEAN,
27 |
28 | FOREIGN KEY(UserID) REFERENCES Users(UserID)
29 | );
30 |
31 | CREATE TABLE IF NOT EXISTS DKIMRecords (
32 | DomainName TEXT NOT NULL,
33 | Selector TEXT NOT NULL, -- "si1", "si2", etc
34 | Current BOOLEAN, -- primary key to use for signing messages
35 | Algorithm TEXT NOT NULL, -- "rsa"
36 | PublicKey TEXT NOT NULL, -- base64 contents of TXT record p= field
37 | PrivateKey TEXT NOT NULL, -- "-----BEGIN RSA PRIVATE KEY-----"
38 |
39 | PRIMARY KEY (DomainName, Selector)
40 | );
41 |
42 | CREATE TABLE IF NOT EXISTS Devices (
43 | DeviceID INTEGER PRIMARY KEY,
44 | UserID INTEGER NOT NULL,
45 | DeviceName TEXT NOT NULL,
46 | AppPassHash TEXT,
47 | Deleted BOOLEAN,
48 | Created INTEGER NOT NULL, -- time.Unix
49 | LastAccessTime INTEGER, -- time.Unix
50 | LastAccessAddr TEXT,
51 |
52 | FOREIGN KEY(UserID) REFERENCES Users(UserID)
53 | );
54 |
55 | CREATE TABLE IF NOT EXISTS Msgs (
56 | StagingID INTEGER PRIMARY KEY,
57 | Sender TEXT NOT NULL,
58 | DKIM TEXT, -- "PASS" for valid signatures
59 | DateReceived INTEGER NOT NULL, -- time.Now.Unix() from the server
60 | ReadyDate INTEGER, -- UnixNano() at moment of DeliveryToProcess -> DeliveryReceived
61 | UserID INTEGER, -- set by createmsg on output messages
62 |
63 | FOREIGN KEY(UserID) REFERENCES Users(UserID)
64 | );
65 |
66 | -- MsgRecipients acts as the "envelope" of a Msg.
67 | CREATE TABLE IF NOT EXISTS MsgRecipients (
68 | StagingID INTEGER NOT NULL,
69 | Recipient TEXT NOT NULL, -- bob@example.com, unique when sending
70 | FullAddress TEXT NOT NULL, -- Bob Doe
71 | DeliveryState INTEGER NOT NULL, -- DeliveryState Go type
72 |
73 | PRIMARY KEY(StagingID, Recipient),
74 | FOREIGN KEY(StagingID) REFERENCES Msgs(StagingID),
75 | FOREIGN KEY(Recipient) REFERENCES UserAddresses(Address)
76 | );
77 |
78 | -- MsgRaw holds the fully-encoded raw contents of a message.
79 | -- It remains entirely unmodified from how it was received.
80 | CREATE TABLE IF NOT EXISTS MsgRaw (
81 | StagingID INTEGER PRIMARY KEY,
82 | Content BLOB,
83 |
84 | FOREIGN KEY(StagingID) REFERENCES Msgs(StagingID)
85 | );
86 |
87 | -- MsgFull holds the fully-encoded raw contents of a message.
88 | -- It has been processed and is ready for delivery.
89 | CREATE TABLE IF NOT EXISTS MsgFull (
90 | StagingID INTEGER PRIMARY KEY,
91 | Content BLOB,
92 |
93 | FOREIGN KEY(StagingID) REFERENCES Msgs(StagingID)
94 | );
95 |
96 | -- Deliveries contains a record for each email delivery attempt made.
97 | -- On successful delivery, Code == 250 and the DeliveryState in MsgRecipients changes.
98 | -- There are many possible codes, a core sample are on https://cr.yp.to/smtp/mail.html.
99 | CREATE TABLE IF NOT EXISTS Deliveries (
100 | AttemptID INTEGER PRIMARY KEY,
101 | StagingID INTEGER NOT NULL,
102 | Recipient TEXT NOT NULL,
103 | Code INTEGER NOT NULL,
104 | Date INTEGER NOT NULL, -- time.Now().Unix()
105 | Details TEXT,
106 |
107 | FOREIGN KEY(StagingID, Recipient) REFERENCES MsgRecipients(StagingID, Recipient)
108 | );
109 | `
110 |
--------------------------------------------------------------------------------
/spilldb/dnsdb/dnsdb.go:
--------------------------------------------------------------------------------
1 | package dnsdb
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "log"
7 | "net"
8 | "regexp"
9 |
10 | "crawshaw.io/sqlite/sqlitex"
11 | "spilled.ink/third_party/dns"
12 | )
13 |
14 | type DNS struct {
15 | DB *sqlitex.Pool
16 | Server *dns.Server // created by DNS.Serve method. TODO unexport?
17 | Logf func(format string, v ...interface{})
18 | }
19 |
20 | func (s *DNS) Serve(ln net.Listener, pc net.PacketConn) error {
21 | if s.Server == nil {
22 | s.Server = &dns.Server{}
23 | }
24 | s.Server.Listener = ln
25 | s.Server.PacketConn = pc
26 | s.Server.Handler = &handler{s: s}
27 | return s.Server.ActivateAndServe()
28 | }
29 |
30 | func (s *DNS) Shutdown(ctx context.Context) error {
31 | return s.Server.ShutdownContext(ctx)
32 | }
33 |
34 | func (s *DNS) lookup(ctx context.Context, queries []query) (result []string, err error) {
35 | conn := s.DB.Get(ctx)
36 | if conn == nil {
37 | return nil, context.Canceled
38 | }
39 | defer s.DB.Put(conn)
40 |
41 | stmt := conn.Prep(`SELECT Algorithm, PublicKey FROM DKIMRecords
42 | WHERE DomainName = $domain AND Selector = $selector;`)
43 |
44 | for _, q := range queries {
45 | stmt.Reset()
46 | stmt.SetText("$domain", q.domain)
47 | stmt.SetText("$selector", q.selector)
48 | if has, err := stmt.Step(); err != nil {
49 | return nil, fmt.Errorf("dnsdb: %s._domainkey.%s: %v", q.selector, q.domain, err)
50 | } else if !has {
51 | continue
52 | }
53 | alg := stmt.GetText("Algorithm")
54 | pubKey := stmt.GetText("PublicKey")
55 | stmt.Reset()
56 |
57 | // TODO: handle TXT field size limit? or do it in dns?
58 | r := fmt.Sprintf("v=DKIM1; k=%s; p=%s", alg, pubKey)
59 | result = append(result, r)
60 | }
61 |
62 | return result, nil
63 | }
64 |
65 | type handler struct {
66 | s *DNS
67 | }
68 |
69 | var domainRE = regexp.MustCompile(`^(.*)._domainkey.(.*).$`)
70 |
71 | type query struct {
72 | selector string
73 | domain string
74 | }
75 |
76 | // Implements dns.Handler
77 | func (s *handler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
78 | var replyToQuestions []dns.Question
79 | var queries []query
80 | // Users set an NS subdomain record for _domainkey..
81 | for _, q := range r.Question {
82 | if q.Qtype != dns.TypeTXT {
83 | log.Printf("DEBUG skipping DNS question %v", q)
84 | continue
85 | }
86 | log.Printf("DEBUG DNS TXT request Name=%s", q.Name)
87 | // We answer "._domainkey." queries here.
88 | //"._domainkey."
89 | match := domainRE.FindStringSubmatch(q.Name)
90 | if len(match) != 3 {
91 | continue
92 | }
93 | queries = append(queries, query{
94 | selector: match[1],
95 | domain: match[2],
96 | })
97 | }
98 | ctx := context.Background() // TODO
99 | result, err := s.s.lookup(ctx, queries)
100 | if err != nil {
101 | s.s.Logf("ServeDNS lookup error: %v", err) // TODO JSON log
102 | return
103 | }
104 |
105 | m := new(dns.Msg)
106 | for i, res := range result {
107 | if res == "" {
108 | continue
109 | }
110 | q := &r.Question[i]
111 | replyToQuestions = append(replyToQuestions, *q)
112 | txt := &dns.TXT{
113 | Hdr: dns.RR_Header{
114 | Name: q.Name,
115 | Rrtype: dns.TypeTXT,
116 | Class: dns.ClassINET,
117 | },
118 | Txt: []string{res},
119 | }
120 | m.Extra = append(m.Extra, txt)
121 | }
122 |
123 | r.Question = replyToQuestions
124 | m.SetReply(r)
125 | if err := w.WriteMsg(m); err != nil {
126 | s.s.Logf("WriteMsg error %v", err) // TODO JSON log
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/spilldb/greylistdb/greylistdb.go:
--------------------------------------------------------------------------------
1 | package greylistdb
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net"
7 | "time"
8 |
9 | "crawshaw.io/sqlite/sqlitex"
10 | "spilled.ink/smtp/smtpserver/greylist"
11 | )
12 |
13 | const dbSQL = `
14 | CREATE TABLE IF NOT EXISTS Greylist (
15 | RemoteAddr TEXT NOT NULL, -- text form of IP address
16 | FromAddr TEXT NOT NULL, -- user@domain
17 | ToAddr TEXT NOT NULL, -- user@domain
18 | LastSeen INTEGER NOT NULL, -- seconds since unix epoch
19 |
20 | PRIMARY KEY (RemoteAddr, FromAddr, ToAddr)
21 | );
22 | `
23 |
24 | // New creates a new Greylist.
25 | //
26 | // After calling New, the caller needs to set the remaining
27 | // exported fields of Greylist before using the NewMessage method.
28 | func New(dbpool *sqlitex.Pool) (*greylist.Greylist, error) {
29 | conn := dbpool.Get(nil)
30 | defer dbpool.Put(conn)
31 | if err := sqlitex.ExecScript(conn, dbSQL); err != nil {
32 | return nil, fmt.Errorf("greylistdb.New: %v", err)
33 | }
34 |
35 | db := &greyDB{
36 | dbpool: dbpool,
37 | }
38 |
39 | gl := &greylist.Greylist{
40 | Whitelist: db.whitelist,
41 | Blacklist: db.blacklist,
42 | GreyDB: db,
43 | }
44 | return gl, nil
45 | }
46 |
47 | type greyDB struct {
48 | dbpool *sqlitex.Pool
49 | }
50 |
51 | func (db *greyDB) Get(ctx context.Context, remoteAddr, from, to string) (time.Time, error) {
52 | conn := db.dbpool.Get(ctx)
53 | if conn == nil {
54 | return time.Time{}, context.Canceled
55 | }
56 | defer db.dbpool.Put(conn)
57 |
58 | stmt := conn.Prep(`SELECT LastSeen FROM Greylist WHERE RemoteAddr = $remoteAddr AND FromAddr = $from AND ToAddr = $to;`)
59 | stmt.SetText("$remoteAddr", remoteAddr)
60 | stmt.SetText("$from", from)
61 | stmt.SetText("$from", to)
62 | if has, err := stmt.Step(); err != nil {
63 | return time.Time{}, err
64 | } else if !has {
65 | return time.Time{}, greylist.ErrNotFound
66 | }
67 | t := time.Unix(stmt.GetInt64("LastSeen"), 0)
68 | stmt.Reset()
69 |
70 | return t, nil
71 | }
72 |
73 | func (db *greyDB) Put(ctx context.Context, remoteAddr, from, to string) error {
74 | conn := db.dbpool.Get(ctx)
75 | if conn == nil {
76 | return context.Canceled
77 | }
78 | defer db.dbpool.Put(conn)
79 |
80 | t := time.Now().Unix()
81 |
82 | stmt := conn.Prep(`INSERT INTO Greylist (
83 | LastSeen, RemoteAddr, FromAddr, ToAddr
84 | ) VALUES (
85 | $lastSeen, $remoteAddr, $fromAddr, $toAddr
86 | ) ON CONFLICT (RemoteAddr, FromAddr, ToAddr)
87 | DO UPDATE Set LastSeen=$lastSeen;`)
88 | stmt.SetInt64("$lastSeen", t)
89 | stmt.SetText("$remoteAddr", remoteAddr)
90 | stmt.SetText("$from", from)
91 | stmt.SetText("$from", to)
92 | _, err := stmt.Step()
93 | return err
94 | }
95 |
96 | func (db *greyDB) whitelist(ctx context.Context, remoteAddr net.Addr, from []byte) (bool, error) {
97 | return false, nil
98 | }
99 |
100 | func (db *greyDB) blacklist(ctx context.Context, remoteAddr net.Addr, from []byte) (bool, error) {
101 | return false, nil
102 | }
103 |
--------------------------------------------------------------------------------
/spilldb/honeypotdb/honeypotdb.go:
--------------------------------------------------------------------------------
1 | // Package honeypotdb wraps an smtpserver NewMessage and
2 | // collects any attempts to authenticate as spam messages.
3 | //
4 | // An SMTP server on port 25 should not accept mail submission
5 | // any more, but lots of AUTH requests still come in.
6 | // This package collects those requests for future study.
7 | package honeypotdb
8 |
9 | import (
10 | "bytes"
11 | "context"
12 | "fmt"
13 | "io"
14 | "math/rand"
15 | "net"
16 | "sync"
17 | "time"
18 |
19 | "crawshaw.io/iox"
20 | "crawshaw.io/sqlite/sqlitex"
21 | "spilled.ink/smtp/smtpserver"
22 | )
23 |
24 | type Honeypot struct {
25 | ctx context.Context
26 | dbpool *sqlitex.Pool
27 | filer *iox.Filer
28 | wrappedNewMsgFn smtpserver.NewMessageFunc
29 |
30 | mu sync.Mutex
31 | auth map[uint64]auth
32 | }
33 |
34 | func (h *Honeypot) cleanup() {
35 | t := time.NewTicker(125 * time.Second)
36 | for {
37 | select {
38 | case <-h.ctx.Done():
39 | t.Stop()
40 | break
41 | case <-t.C:
42 | h.mu.Lock()
43 | for token, a := range h.auth {
44 | if time.Since(a.t) > 120*time.Second {
45 | delete(h.auth, token)
46 | }
47 | }
48 | h.mu.Unlock()
49 | }
50 | }
51 | }
52 |
53 | type auth struct {
54 | t time.Time
55 | identity string
56 | user string
57 | pass string
58 | remoteAddr string
59 | }
60 |
61 | func New(ctx context.Context, dbpool *sqlitex.Pool, filer *iox.Filer, newMsgFn smtpserver.NewMessageFunc) (*Honeypot, error) {
62 | conn := dbpool.Get(ctx)
63 | if conn == nil {
64 | return nil, context.Canceled
65 | }
66 | defer dbpool.Put(conn)
67 |
68 | err := sqlitex.ExecTransient(conn, `CREATE TABLE IF NOT EXISTS Honeypot (
69 | HoneypotID INTEGER PRIMARY KEY,
70 | Date INTEGER NOT NULL,
71 | RemoteAddr TEXT NOT NULL,
72 | FromAddr TEXT NOT NULL,
73 | Recipients TEXT NOT NULL, -- JSON array of strings
74 | Identity TEXT NOT NULL,
75 | User TEXT NOT NULL,
76 | Password TEXT NOT NULL,
77 | Contents BLOB NOT NULL
78 | );`, nil)
79 | if err != nil {
80 | return nil, err
81 | }
82 |
83 | h := &Honeypot{
84 | ctx: ctx,
85 | dbpool: dbpool,
86 | filer: filer,
87 | wrappedNewMsgFn: newMsgFn,
88 | auth: make(map[uint64]auth),
89 | }
90 | go h.cleanup()
91 | return h, nil
92 | }
93 |
94 | func (h *Honeypot) Auth(identity, user, pass []byte, remoteAddr string) uint64 {
95 | h.mu.Lock()
96 | var token uint64
97 | for token == 0 || !h.auth[token].t.IsZero() {
98 | token = rand.Uint64()
99 | }
100 | h.auth[token] = auth{
101 | t: time.Now(),
102 | identity: string(identity),
103 | user: string(user),
104 | pass: string(pass),
105 | remoteAddr: remoteAddr,
106 | }
107 | h.mu.Unlock()
108 |
109 | // Any auth request succeeds.
110 | time.Sleep(2 * time.Second) // malicious client delay
111 | return token
112 | }
113 |
114 | func (h *Honeypot) NewMessage(remoteAddr net.Addr, from []byte, token uint64) (smtpserver.Msg, error) {
115 | if token == 0 {
116 | // This is a real message.
117 | return h.wrappedNewMsgFn(remoteAddr, from, 0)
118 | }
119 |
120 | h.mu.Lock()
121 | a := h.auth[token]
122 | delete(h.auth, token)
123 | h.mu.Unlock()
124 |
125 | return &msg{
126 | ctx: h.ctx,
127 | dbpool: h.dbpool,
128 | f: h.filer.BufferFile(0),
129 | auth: a,
130 | remoteAddr: remoteAddr,
131 | from: string(from),
132 | }, nil
133 | }
134 |
135 | type msg struct {
136 | ctx context.Context
137 | dbpool *sqlitex.Pool
138 | f *iox.BufferFile
139 | rcpts []string
140 | remoteAddr net.Addr
141 | auth auth
142 | from string
143 | }
144 |
145 | func (m *msg) AddRecipient(addr []byte) (bool, error) {
146 | // Pretend to be an open relay.
147 | m.rcpts = append(m.rcpts, string(addr))
148 | time.Sleep(time.Second / 2) // malicious client delay
149 | return true, nil
150 | }
151 |
152 | func (m *msg) Write(line []byte) error {
153 | time.Sleep(50 * time.Millisecond) // malicious client delay
154 | _, err := m.f.Write(line)
155 | return err
156 | }
157 |
158 | func (m *msg) Cancel() {
159 | m.f.Close()
160 | m.rcpts = nil
161 | }
162 |
163 | func (m *msg) Close() error {
164 | defer time.Sleep(2 * time.Second) // malicious client delay
165 | defer m.f.Close()
166 |
167 | if _, err := m.f.Seek(0, 0); err != nil {
168 | return err
169 | }
170 |
171 | rcpts := new(bytes.Buffer)
172 | rcpts.WriteByte('[')
173 | for i, rcpt := range m.rcpts {
174 | if i > 0 {
175 | rcpts.WriteString(", ")
176 | }
177 | fmt.Fprintf(rcpts, "%q", rcpt)
178 | }
179 | rcpts.WriteByte(']')
180 |
181 | conn := m.dbpool.Get(m.ctx)
182 | if conn == nil {
183 | return context.Canceled
184 | }
185 | defer m.dbpool.Put(conn)
186 |
187 | stmt := conn.Prep(`INSERT INTO Honeypot (
188 | Date, RemoteAddr, FromAddr, Recipients, Identity, User, Password, Contents
189 | ) VALUES (
190 | $date, $remoteAddr, $fromAddr, $recipients, $identity, $user, $password, $contents
191 | );`)
192 | stmt.SetInt64("$date", m.auth.t.Unix())
193 | stmt.SetText("$remoteAddr", m.auth.remoteAddr)
194 | stmt.SetText("$fromAddr", m.from)
195 | stmt.SetBytes("$recipients", rcpts.Bytes())
196 | stmt.SetText("$identity", m.auth.identity)
197 | stmt.SetText("$user", m.auth.user)
198 | stmt.SetText("$password", m.auth.pass)
199 | stmt.SetZeroBlob("$contents", m.f.Size())
200 | if _, err := stmt.Step(); err != nil {
201 | return err
202 | }
203 | honeypotID := conn.LastInsertRowID()
204 |
205 | b, err := conn.OpenBlob("", "Honeypot", "Contents", honeypotID, true)
206 | if err != nil {
207 | return err
208 | }
209 | _, err = io.Copy(b, m.f)
210 | if closeErr := b.Close(); err == nil {
211 | err = closeErr
212 | }
213 | return err
214 | }
215 |
--------------------------------------------------------------------------------
/spilldb/imapdb/imapdb_test.go:
--------------------------------------------------------------------------------
1 | package imapdb
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "io"
8 | "io/ioutil"
9 | "os"
10 | "path/filepath"
11 | "runtime/trace"
12 | "strings"
13 | "testing"
14 | "time"
15 |
16 | "crawshaw.io/iox"
17 | "crawshaw.io/sqlite/sqlitex"
18 | "spilled.ink/email/msgcleaver"
19 | "spilled.ink/imap"
20 | "spilled.ink/imap/imapserver"
21 | "spilled.ink/imap/imaptest"
22 | "spilled.ink/spilldb/boxmgmt"
23 | "spilled.ink/spilldb/db"
24 | )
25 |
26 | const tracing = false
27 |
28 | func Test(t *testing.T) {
29 | if tracing {
30 | f, err := os.Create("trace.out")
31 | if err != nil {
32 | t.Fatalf("failed to create trace output file: %v", err)
33 | }
34 | defer func() {
35 | if err := f.Close(); err != nil {
36 | t.Fatalf("failed to close trace file: %v", err)
37 | }
38 | }()
39 |
40 | if err := trace.Start(f); err != nil {
41 | t.Fatalf("failed to start trace: %v", err)
42 | }
43 | defer trace.Stop()
44 | }
45 |
46 | filer := iox.NewFiler(0)
47 | filer.DefaultBufferMemSize = 1 << 20
48 | filer.Logf = t.Logf
49 | defer func() {
50 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
51 | defer cancel()
52 | filer.Shutdown(ctx)
53 | }()
54 |
55 | t.Run("imapdb", func(t *testing.T) {
56 | for _, test := range imaptest.Tests {
57 | test := test
58 | t.Run(test.Name, func(t *testing.T) {
59 | t.Parallel()
60 | dataStore, err := newDataStore(filer, t.Logf)
61 | if err != nil {
62 | t.Fatal(err)
63 | }
64 | server, err := imaptest.InitTestServer(filer, dataStore, dataStore)
65 | if err != nil {
66 | t.Fatal(err)
67 | }
68 | defer func() {
69 | if err := server.Shutdown(); err != nil {
70 | t.Fatal(err)
71 | }
72 | dataStore.Close()
73 | }()
74 |
75 | test.Fn(t, server)
76 | })
77 | }
78 | })
79 | }
80 |
81 | type dataStore struct {
82 | backend *backend
83 | dbpool *sqlitex.Pool
84 | nextStagingID int64
85 | }
86 |
87 | func (ds *dataStore) Login(c *imapserver.Conn, username, password []byte) (int64, imap.Session, error) {
88 | return ds.backend.Login(c, username, password)
89 | }
90 |
91 | func (ds *dataStore) RegisterNotifier(notifier imap.Notifier) {
92 | ds.backend.RegisterNotifier(notifier)
93 | }
94 |
95 | func (ds *dataStore) Close() {
96 | ds.dbpool.Close()
97 | }
98 |
99 | func (ds *dataStore) AddUser(username, password []byte) (err error) {
100 | fmt.Printf("dataStore.AddUser(%s, %s)\n", username, password)
101 | ctx := context.Background()
102 |
103 | conn := ds.dbpool.Get(ctx)
104 | if conn == nil {
105 | return context.Canceled
106 | }
107 | defer ds.dbpool.Put(conn)
108 |
109 | userID, err := db.AddUser(conn, db.UserDetails{
110 | EmailAddr: string(username),
111 | Password: "agenericpassword",
112 | })
113 | pwd := strings.ToUpper(string(password))
114 | if _, err := db.AddDevice(conn, userID, "testdevice", pwd); err != nil {
115 | return err
116 | }
117 |
118 | user, err := ds.backend.boxmgmt.Open(ctx, userID)
119 | if err != nil {
120 | return err
121 | }
122 | if err := user.Box.Init(ctx); err != nil {
123 | return err
124 | }
125 | return nil
126 | }
127 |
128 | func (ds *dataStore) SendMsg(date time.Time, data io.Reader) error {
129 | msg, err := msgcleaver.Cleave(ds.backend.filer, data)
130 | if err != nil {
131 | return fmt.Errorf("SendMsg: %v", err)
132 | }
133 | msg.Date = date
134 |
135 | addr := string(msg.Headers.Get("To"))
136 | userID, err := ds.getUserID(addr)
137 | if err != nil {
138 | return fmt.Errorf("SendMsg: %v", err)
139 | }
140 |
141 | ctx := context.Background()
142 | user, err := ds.backend.boxmgmt.Open(ctx, userID)
143 | if err != nil {
144 | return fmt.Errorf("SendMsg: %v", err)
145 | }
146 | done, err := user.Box.InsertMsg(ctx, msg, ds.nextStagingID)
147 | ds.nextStagingID++
148 | if err != nil {
149 | return err
150 | }
151 | if !done {
152 | return errors.New("SendMsg: missing message content")
153 | }
154 | return nil
155 | }
156 |
157 | func (ds *dataStore) getUserID(addr string) (int64, error) {
158 | conn := ds.dbpool.Get(nil)
159 | defer ds.dbpool.Put(conn)
160 |
161 | stmt := conn.Prep("SELECT UserID FROM UserAddresses WHERE Address = $addr;")
162 | stmt.SetText("$addr", addr)
163 | return sqlitex.ResultInt64(stmt)
164 | }
165 |
166 | func newDataStore(filer *iox.Filer, logf func(format string, v ...interface{})) (*dataStore, error) {
167 | dir, err := ioutil.TempDir("", "imapdb-test-")
168 | if err != nil {
169 | return nil, err
170 | }
171 | logf("data store tempdir: %s", dir)
172 | dbpool, err := db.Open(filepath.Join(dir, "spilld.db"))
173 | if err != nil {
174 | return nil, err
175 | }
176 |
177 | boxMgmt, err := boxmgmt.New(filer, dbpool, dir)
178 | if err != nil {
179 | return nil, fmt.Errorf("bomgmt: %v", err)
180 | }
181 |
182 | ds := &dataStore{
183 | backend: NewBackend(dbpool, filer, boxMgmt, logf).(*backend),
184 | dbpool: dbpool,
185 | nextStagingID: 42,
186 | }
187 | return ds, nil
188 | }
189 |
--------------------------------------------------------------------------------
/spilldb/spillbox/contact.go:
--------------------------------------------------------------------------------
1 | package spillbox
2 |
3 | import (
4 | "crawshaw.io/sqlite"
5 | "spilled.ink/email"
6 | )
7 |
8 | // ResolveAddressID computes a DB AddressID and ContactID for an email address.
9 | //
10 | // If no existing contact record is found, one is created.
11 | // Address normalization is used to match new addresses to existing
12 | // contacts if it is possible.
13 | func ResolveAddressID(conn *sqlite.Conn, addr *email.Address, visible bool) (addressID AddressID, contactID ContactID, err error) {
14 | var visibleInDB bool
15 |
16 | stmt := conn.Prep("SELECT AddressID, ContactID, Visible FROM Addresses WHERE Name = $name AND Address = $addr;")
17 | stmt.SetText("$name", addr.Name)
18 | stmt.SetText("$addr", addr.Addr)
19 | if hasNext, err := stmt.Step(); err != nil {
20 | return 0, 0, err
21 | } else if hasNext {
22 | addressID = AddressID(stmt.GetInt64("AddressID"))
23 | contactID = ContactID(stmt.GetInt64("ContactID"))
24 | visibleInDB = stmt.GetInt64("Visible") > 0
25 | stmt.Reset()
26 | }
27 |
28 | if contactID == 0 {
29 | // Try to find a contact with a name variant.
30 | stmt := conn.Prep("SELECT ContactID FROM Addresses WHERE Address = $addr;")
31 | stmt.SetText("$addr", addr.Addr)
32 | if hasNext, err := stmt.Step(); err != nil {
33 | return 0, 0, err
34 | } else if hasNext {
35 | contactID = ContactID(stmt.GetInt64("ContactID"))
36 | stmt.Reset()
37 | }
38 | }
39 |
40 | normAddr := string(normalizeAddr([]byte(addr.Addr)))
41 | if contactID == 0 {
42 | // Try to find a contact with a normalized addr.
43 | stmt := conn.Prep("SELECT ContactID FROM Addresses WHERE Address = $addr;")
44 | stmt.SetText("$addr", normAddr)
45 | if hasNext, err := stmt.Step(); err != nil {
46 | return 0, 0, err
47 | } else if hasNext {
48 | contactID = ContactID(stmt.GetInt64("ContactID"))
49 | stmt.Reset()
50 | }
51 | }
52 |
53 | defaultAddr := false
54 | if contactID == 0 {
55 | // We have never seen the address before or its like, so make a new contact.
56 | // TODO: take an initially better guess at Robot?
57 | stmt := conn.Prep("INSERT INTO Contacts (ContactID, Robot) VALUES ($contactID, FALSE);")
58 | if id, err := InsertRandID(stmt, "$contactID"); err != nil {
59 | return 0, 0, err
60 | } else {
61 | contactID = ContactID(id)
62 | }
63 | defaultAddr = true
64 | }
65 |
66 | if addressID == 0 {
67 | // New addr (even if a non-normal variant), add for the contact.
68 | stmt := conn.Prep(`INSERT INTO Addresses (AddressID, ContactID, Name, Address, DefaultAddr, Visible)
69 | VALUES ($addressID, $contactID, $name, $addr, $defaultAddr, $visible);`)
70 | stmt.SetInt64("$contactID", int64(contactID))
71 | stmt.SetText("$name", addr.Name)
72 | stmt.SetText("$addr", addr.Addr)
73 | stmt.SetBool("$visible", visible)
74 | stmt.SetBool("$defaultAddr", defaultAddr)
75 | if id, err := InsertRandID(stmt, "$addressID"); err != nil {
76 | return 0, 0, err
77 | } else {
78 | addressID = AddressID(id)
79 | }
80 | visibleInDB = visible
81 | stmt.Reset()
82 |
83 | if normAddr != addr.Addr {
84 | stmt.SetText("$addr", normAddr)
85 | stmt.SetBool("$visible", false)
86 | stmt.SetBool("$defaultAddr", false)
87 | if _, err := InsertRandID(stmt, "$addressID"); err != nil {
88 | return 0, 0, err
89 | }
90 | stmt.Reset()
91 | }
92 | }
93 |
94 | if visible && !visibleInDB {
95 | stmt := conn.Prep("UPDATE Addresses SET Visible = $visible WHERE AddressID = $addressID;")
96 | stmt.SetBool("$visible", visible)
97 | stmt.SetInt64("$addressID", int64(addressID))
98 | if _, err := stmt.Step(); err != nil {
99 | return 0, 0, err
100 | }
101 | }
102 |
103 | return addressID, contactID, nil
104 | }
105 |
--------------------------------------------------------------------------------
/spilldb/spillbox/mailbox.go:
--------------------------------------------------------------------------------
1 | package spillbox
2 |
3 | import (
4 | "fmt"
5 | "path/filepath"
6 | "strings"
7 |
8 | "crawshaw.io/sqlite"
9 | "crawshaw.io/sqlite/sqlitex"
10 | "spilled.ink/imap"
11 | )
12 |
13 | func CreateMailbox(conn *sqlite.Conn, name string, attr imap.ListAttrFlag) (err error) {
14 | defer sqlitex.Save(conn)(&err)
15 |
16 | for _, res := range noKidsMailboxes {
17 | if strings.HasPrefix(name, res) && len(name) > len(res) && name[len(res)] == '/' {
18 | return fmt.Errorf("spillbox.CreateMailbox(%q): cannot create mailbox under %q", name, res)
19 | }
20 | }
21 |
22 | stmt := conn.Prep(`INSERT INTO Mailboxes (
23 | MailboxID, NextUID, UIDValidity, Name, Attrs
24 | ) VALUES (
25 | $id, 1,
26 | coalesce((SELECT max(UIDValidity) FROM Mailboxes), 42) + 1,
27 | $name, $attrs);`)
28 | stmt.SetText("$name", name)
29 | stmt.SetInt64("$attrs", int64(attr))
30 | if _, err := InsertRandID(stmt, "$id"); err != nil {
31 | if sqlite.ErrCode(err) == sqlite.SQLITE_CONSTRAINT_UNIQUE {
32 | return fmt.Errorf("spillbox.CreateMailbox(%q): exists", name)
33 | }
34 | return fmt.Errorf("spillbox.CreateMailbox(%q): %v", name, err)
35 | }
36 |
37 | stmt = conn.Prep(`INSERT OR IGNORE INTO MailboxSequencing
38 | (Name, NextModSequence) VALUES ($name, 1);`)
39 | stmt.SetText("$name", name)
40 | if _, err := stmt.Step(); err != nil {
41 | return err
42 | }
43 |
44 | outer := name
45 | for {
46 | outer = filepath.Dir(outer)
47 | if outer == "." || outer == "INBOX" {
48 | break
49 | }
50 |
51 | stmt.Reset()
52 | stmt.SetText("$name", outer)
53 | _, err := InsertRandID(stmt, "$id")
54 | if sqlite.ErrCode(err) == sqlite.SQLITE_CONSTRAINT_UNIQUE {
55 | break // outer dir exists
56 | }
57 | if err != nil {
58 | return fmt.Errorf("CreateMailbox(%q) outer name %q failed: %v", name, outer, err)
59 | }
60 | }
61 |
62 | return nil
63 |
64 | }
65 |
66 | func DeleteMailbox(conn *sqlite.Conn, name string) (err error) {
67 | if reservedMailboxNames[name] {
68 | return fmt.Errorf("spillbox.DeleteMailbox: cannot delete %q", name)
69 | }
70 | stmt := conn.Prep(`UPDATE Mailboxes SET DeletedName = Name, Name = NULL
71 | WHERE Name = $name;`)
72 | stmt.SetText("$name", name)
73 | if _, err := stmt.Step(); err != nil {
74 | return fmt.Errorf("spillbox.DeleteMailbox(%q): %v", name, err)
75 | }
76 | if conn.Changes() == 0 {
77 | return fmt.Errorf("spillbox.DeleteMailbox(%q): no such mailbox", name)
78 | }
79 | return nil
80 | }
81 |
82 | var noKidsMailboxes = []string{
83 | "INBOX",
84 | "Archive",
85 | "Sent",
86 | "Drafts",
87 | "Trash",
88 | }
89 |
90 | var reservedMailboxNames = map[string]bool{
91 | "Subscriptions": true,
92 | }
93 |
94 | func init() {
95 | for _, n := range noKidsMailboxes {
96 | reservedMailboxNames[n] = true
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/spilldb/spillbox/normalize.go:
--------------------------------------------------------------------------------
1 | package spillbox
2 |
3 | import "bytes"
4 |
5 | // normalizeAddr normalizes email addresses.
6 | // The new address is written inline into the provided bytes.
7 | //
8 | // Some email hosts have features that automatically map an arbitrary
9 | // number of addresses onto one. In particular for gmail:
10 | //
11 | // joe.smith@googlemail.com -> joesmith@gmail.com
12 | // joesmith+hello@gmail.com -> joesmith@gmail.com
13 | //
14 | // (Details https://gmail.googleblog.com/2008/03/2-hidden-ways-to-get-more-from-your.html)
15 | //
16 | // This function maps email addresses onto a minimal normal form,
17 | // which helps with contact matching.
18 | func normalizeAddr(addr []byte) (norm []byte) {
19 | // Ignore bad email addresses.
20 | if len(addr) == 0 {
21 | return addr
22 | }
23 | i := bytes.IndexByte(addr, '@')
24 | if i == -1 || i == len(addr)-1 {
25 | return addr
26 | }
27 |
28 | user, domain := addr[:i], addr[i+1:]
29 |
30 | if domain[len(domain)-1] == '.' {
31 | domain = domain[:len(domain)-1]
32 | }
33 | domain = bytes.ToLower(domain)
34 | if d, hasAlias := domainAliases[string(domain)]; hasAlias {
35 | domain = d
36 | }
37 |
38 | details := domainDetails[string(domain)]
39 | if details.IgnoreUserDots {
40 | u := user[:0] // we are strictly shortening user
41 | for _, b := range user {
42 | switch b {
43 | case '+':
44 | break
45 | case '.':
46 | // ignore
47 | default:
48 | u = append(u, b)
49 | }
50 | }
51 | user = u
52 | }
53 | if details.Caseless {
54 | // TODO: investigate whether servers support caseless UTF-8, RFC6531.
55 | asciiLower(user)
56 | }
57 |
58 | addr = addr[:0]
59 | addr = append(addr, user...)
60 | addr = append(addr, '@')
61 | addr = append(addr, domain...)
62 |
63 | return addr
64 | }
65 |
66 | func asciiLower(data []byte) {
67 | for i, b := range data {
68 | if b >= 'A' && b <= 'Z' {
69 | data[i] = b + ('a' - 'A')
70 | }
71 | }
72 | }
73 |
74 | type domainDetail struct {
75 | Caseless bool
76 | IgnoreUserDots bool
77 | }
78 |
79 | var domainDetails = map[string]domainDetail{
80 | "meetup.com": domainDetail{Caseless: true},
81 | "yahoo.com": domainDetail{Caseless: true},
82 | "hotmail.com": domainDetail{Caseless: true},
83 | "aol.com": domainDetail{Caseless: true},
84 | "msn.com": domainDetail{Caseless: true},
85 | "outlook.com": domainDetail{Caseless: true},
86 | "facebook.com": domainDetail{Caseless: true},
87 | "live.com": domainDetail{Caseless: true},
88 | "comcast.net": domainDetail{Caseless: true},
89 | "earthlink.net": domainDetail{Caseless: true},
90 | "gmail.com": domainDetail{Caseless: true, IgnoreUserDots: true},
91 | "zentus.com": domainDetail{Caseless: true, IgnoreUserDots: true},
92 | "googlegroups.com": domainDetail{Caseless: true, IgnoreUserDots: true},
93 | }
94 |
95 | var domainAliases = map[string][]byte{
96 | "googlemail.com": []byte("gmail.com"),
97 | }
98 |
--------------------------------------------------------------------------------
/spilldb/spillbox/prettyhtml/prettyhtml.go:
--------------------------------------------------------------------------------
1 | // TODO: this is a stub package for readability-style transformations of HTML.
2 | // TODO: be aggresive towards unsubscribe-based mail, e.g. remove animated GIFS
3 | package prettyhtml
4 |
5 | import (
6 | "bytes"
7 | "io"
8 | "net/url"
9 | "regexp"
10 | "strings"
11 |
12 | "golang.org/x/net/html"
13 | "spilled.ink/html/htmlsafe"
14 | )
15 |
16 | type Prettifier struct {
17 | }
18 |
19 | func New() (*Prettifier, error) {
20 | p := &Prettifier{}
21 | return p, nil
22 | }
23 |
24 | type Result struct {
25 | HTML string
26 | HasUnsubscribe bool
27 | }
28 |
29 | var unsubRE = regexp.MustCompile(`(?i)unsubscribe`)
30 |
31 | // Pretty cleans up the given HTML and makes it readable.
32 | // On error, some reasonable HTML value is always returned
33 | func (p *Prettifier) Pretty(r io.Reader, contentLinks map[string]string) (Result, error) {
34 | rewrite := func(attr string, url *url.URL) string {
35 | if url.Scheme == "cid" && contentLinks != nil {
36 | return contentLinks[url.Opaque]
37 | }
38 | return url.String()
39 | }
40 | s := htmlsafe.Sanitizer{RewriteURL: rewrite}
41 | buf := new(bytes.Buffer)
42 | if _, err := s.Sanitize(buf, r); err != nil {
43 | return Result{HTML: "failed to sanitize"}, err
44 | }
45 | orderly := buf.String()
46 |
47 | res := Result{
48 | HTML: orderly,
49 | }
50 |
51 | node, err := html.Parse(strings.NewReader(orderly))
52 | if err != nil {
53 | return Result{HTML: orderly}, err
54 | }
55 |
56 | // Analyse body for .*unsubscribe.*
57 | inLink := false
58 | var findUnsub func(*html.Node)
59 | findUnsub = func(n *html.Node) {
60 | if n.Type == html.ElementNode && n.Data == "a" {
61 | inLink = true //
62 | }
63 | if inLink && n.Type == html.TextNode {
64 | if unsubRE.MatchString(n.Data) {
65 | res.HasUnsubscribe = true
66 | }
67 | }
68 | for c := n.FirstChild; c != nil; c = c.NextSibling {
69 | findUnsub(c)
70 | }
71 | if n.Type == html.ElementNode && n.Data == "a" {
72 | inLink = false
73 | }
74 | }
75 | findUnsub(node)
76 |
77 | return res, nil
78 | }
79 |
80 | // PlainText processes HTML into plain text.
81 | // ALl newlines are CRLFs, just the way email likes it.
82 | func PlainText(dst io.Writer, src io.Reader) error {
83 | z := html.NewTokenizer(src)
84 | pendingNewlines := 0
85 | for {
86 | tt := z.Next()
87 | switch tt {
88 | case html.ErrorToken:
89 | if err := z.Err(); err == io.EOF {
90 | return nil
91 | } else {
92 | return err
93 | }
94 | case html.TextToken:
95 | for pendingNewlines > 0 {
96 | dst.Write(newline)
97 | pendingNewlines--
98 | }
99 | if _, err := dst.Write(z.Text()); err != nil {
100 | return err
101 | }
102 | case html.StartTagToken:
103 | tn, _ := z.TagName()
104 | switch {
105 | case len(tn) == 3 && tn[0] == 'd' && tn[1] == 'i' && tn[2] == 'v':
106 | fallthrough
107 | case len(tn) == 1 && tn[0] == 'p':
108 | pendingNewlines++
109 | }
110 | }
111 | }
112 | }
113 |
114 | var newline = []byte{'\r', '\n'}
115 |
--------------------------------------------------------------------------------
/spilldb/spillbox/prettyhtml/prettyhtml_test.go:
--------------------------------------------------------------------------------
1 | package prettyhtml
2 |
3 | import (
4 | "bytes"
5 | "strings"
6 | "testing"
7 | )
8 |
9 | func TestPlainText(t *testing.T) {
10 | const html = "Here is some HTML to convert to plain text " +
11 | "version.Next line.
Next " +
12 | "paragraph.
This is bold" +
13 | ", italic, and underlined text." +
14 | "
Regards.
" +
15 | "
" +
16 | "
"
17 |
18 | want := strings.Replace(`Here is some HTML to convert to plain text version.
19 | Next line.
20 |
21 | Next paragraph.
22 |
23 | This is bold, italic, and underlined text.
24 |
25 | Regards.`, "\n", "\r\n", -1)
26 |
27 | buf := new(bytes.Buffer)
28 | if err := PlainText(buf, strings.NewReader(html)); err != nil {
29 | t.Fatal(err)
30 | }
31 | got := buf.String()
32 | if got != want {
33 | t.Errorf("PlainText()=\n%s\n\nwant:\n%s", got, want)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/spilldb/spillbox/printmsg.go:
--------------------------------------------------------------------------------
1 | //+build ignore
2 |
3 | package main
4 |
5 | import (
6 | "io"
7 | "log"
8 | "os"
9 |
10 | "crawshaw.io/iox"
11 | "spilled.ink/spilldb/spillbox"
12 | )
13 |
14 | func main() {
15 | sbox, err := spillbox.New(os.Args[1], 1)
16 | if err != nil {
17 | log.Fatal(err)
18 | }
19 | defer sbox.Close()
20 |
21 | msgID, err := spillbox.ParseMsgID(os.Args[2])
22 | if err != nil {
23 | log.Fatal(err)
24 | }
25 |
26 | conn := sbox.PoolRO.Get(nil)
27 | defer sbox.PoolRO.Put(conn)
28 |
29 | filer := iox.NewFiler(0)
30 |
31 | buf, err := spillbox.BuildMessage(conn, filer, msgID)
32 | if err != nil {
33 | log.Fatal(err)
34 | }
35 | io.Copy(os.Stdout, buf)
36 | }
37 |
--------------------------------------------------------------------------------
/spilldb/webcache/webcache.go:
--------------------------------------------------------------------------------
1 | package webcache
2 |
3 | import (
4 | "context"
5 | "io"
6 | "net/http"
7 | "time"
8 |
9 | "crawshaw.io/iox"
10 | "crawshaw.io/iox/webfetch"
11 | "crawshaw.io/sqlite/sqlitex"
12 | )
13 |
14 | func New(dbpool *sqlitex.Pool, filer *iox.Filer, httpClient *http.Client, logf func(format string, v ...interface{})) (*webfetch.Client, error) {
15 | conn := dbpool.Get(nil)
16 | defer dbpool.Put(conn)
17 |
18 | err := sqlitex.ExecTransient(conn, `CREATE TABLE IF NOT EXISTS WebCache (
19 | URL TEXT PRIMARY KEY,
20 | FetchTime INTEGER NOT NULL, -- seconds since epoc, time.Now().Unix()
21 | ContentType TEXT,
22 | Content BLOB
23 | );`, nil)
24 | if err != nil {
25 | return nil, err
26 | }
27 |
28 | c := cache{dbpool}
29 | return &webfetch.Client{
30 | Filer: filer,
31 | Client: httpClient,
32 | Logf: logf,
33 | CacheGet: c.get,
34 | CachePut: c.put,
35 | }, nil
36 | }
37 |
38 | type cache struct {
39 | dbpool *sqlitex.Pool
40 | }
41 |
42 | func (c cache) get(ctx context.Context, dst io.Writer, url string) (bool, string, error) {
43 | conn := c.dbpool.Get(ctx)
44 | if conn == nil {
45 | return false, "", context.Canceled
46 | }
47 | defer c.dbpool.Put(conn)
48 |
49 | stmt := conn.Prep("SELECT rowid, ContentType FROM WebCache WHERE URL = $url;")
50 | stmt.SetText("$url", url)
51 | if found, err := stmt.Step(); err != nil {
52 | return false, "", err
53 | } else if !found {
54 | return false, "", nil
55 | }
56 | rowID := stmt.GetInt64("rowid")
57 | contentType := stmt.GetText("ContentType")
58 | stmt.Reset()
59 |
60 | blob, err := conn.OpenBlob("", "Webcache", "Content", rowID, false)
61 | if err != nil {
62 | return false, "", err
63 | }
64 | defer blob.Close()
65 |
66 | if _, err := io.Copy(dst, blob); err != nil {
67 | return false, "", err
68 | }
69 | return true, contentType, nil
70 | }
71 |
72 | func (c cache) put(ctx context.Context, url, contentType string, src io.Reader, srcLen int64) (err error) {
73 | conn := c.dbpool.Get(ctx)
74 | if conn == nil {
75 | return context.Canceled
76 | }
77 | defer c.dbpool.Put(conn)
78 | defer sqlitex.Save(conn)(&err)
79 |
80 | stmt := conn.Prep(`INSERT INTO WebCache (
81 | URL, FetchTime, ContentType, Content
82 | ) VALUES (
83 | $url, $fetchTime, $contentType, $content
84 | );`)
85 | stmt.SetText("$url", url)
86 | if contentType != "" {
87 | stmt.SetText("$contentType", contentType)
88 | } else {
89 | stmt.SetNull("$contentType")
90 | }
91 | stmt.SetInt64("$fetchTime", time.Now().Unix())
92 | stmt.SetZeroBlob("$content", srcLen)
93 | if _, err := stmt.Step(); err != nil {
94 | return err
95 | }
96 | rowID := conn.LastInsertRowID()
97 |
98 | blob, err := conn.OpenBlob("", "Webcache", "Content", rowID, true)
99 | if err != nil {
100 | return err
101 | }
102 | _, err = io.Copy(blob, src)
103 | if err := blob.Close(); err != nil {
104 | return err
105 | }
106 | return nil
107 | }
108 |
--------------------------------------------------------------------------------
/spilldb/webcache/webcache_test.go:
--------------------------------------------------------------------------------
1 | package webcache
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "io"
7 | "io/ioutil"
8 | "net/http"
9 | "net/http/httptest"
10 | "sync"
11 | "testing"
12 |
13 | "crawshaw.io/iox"
14 | "crawshaw.io/sqlite"
15 | "crawshaw.io/sqlite/sqlitex"
16 | )
17 |
18 | func mkdb(t *testing.T) *sqlitex.Pool {
19 | t.Helper()
20 |
21 | flags := sqlite.SQLITE_OPEN_READWRITE | sqlite.SQLITE_OPEN_CREATE | sqlite.SQLITE_OPEN_SHAREDCACHE | sqlite.SQLITE_OPEN_URI
22 | dbpool, err := sqlitex.Open("file::memory:?mode=memory&cache=shared", flags, 8)
23 | if err != nil {
24 | t.Fatal(err)
25 | }
26 |
27 | return dbpool
28 | }
29 |
30 | func TestWebCache(t *testing.T) {
31 | block := make(chan struct{})
32 | close(block)
33 |
34 | saw := make(map[string]int)
35 |
36 | handler := func(w http.ResponseWriter, r *http.Request) {
37 | saw[r.URL.Path]++
38 | w.Header().Set("Content-Type", "text/plain")
39 | io.WriteString(w, "content")
40 | }
41 | ts := httptest.NewTLSServer(http.HandlerFunc(handler))
42 | defer ts.Close()
43 |
44 | filer := iox.NewFiler(0)
45 | dbpool := mkdb(t)
46 | defer func() {
47 | if err := dbpool.Close(); err != nil {
48 | t.Error(err)
49 | }
50 | }()
51 |
52 | webclient, err := New(dbpool, filer, ts.Client(), nil)
53 | if err != nil {
54 | t.Fatal(err)
55 | }
56 | defer webclient.Shutdown(context.Background())
57 |
58 | do := func() {
59 | req, err := http.NewRequest("GET", ts.URL, nil)
60 | if err != nil {
61 | t.Fatal(err)
62 | }
63 | res, err := webclient.Do(req)
64 | if err != nil {
65 | t.Fatal(err)
66 | }
67 | body, err := ioutil.ReadAll(res.Body)
68 | res.Body.Close()
69 | if err != nil {
70 | t.Fatal(err)
71 | }
72 | if got, want := res.Header.Get("Content-Type"), "text/plain"; got != want {
73 | t.Errorf("Content-Type: %q, want %q", got, want)
74 | }
75 | if got, want := string(body), "content"; got != want {
76 | t.Errorf("got %q, want %q", got, want)
77 | }
78 | }
79 |
80 | do() // fills cache
81 |
82 | const want = 1
83 | if got := saw["/"]; got != want {
84 | t.Errorf(`saw["/"]=%d, want %d`, got, want)
85 | }
86 |
87 | do() // hits cache
88 |
89 | if got := saw["/"]; got != want {
90 | t.Errorf(`saw["/"]=%d, want %d`, got, want)
91 | }
92 | }
93 |
94 | // TODO: this is a duplicate of code in iox/webfetch/webfetch_test.go.
95 | func TestConcurrency(t *testing.T) {
96 | handler := func(w http.ResponseWriter, r *http.Request) {
97 | io.WriteString(w, "contentof:")
98 | io.WriteString(w, r.URL.Path)
99 | }
100 | ts := httptest.NewTLSServer(http.HandlerFunc(handler))
101 | defer ts.Close()
102 |
103 | newReq := func(path string) *http.Request {
104 | req, err := http.NewRequest("GET", ts.URL+path, nil)
105 | if err != nil {
106 | t.Fatal(err)
107 | }
108 | return req
109 | }
110 |
111 | filer := iox.NewFiler(0)
112 | dbpool := mkdb(t)
113 | defer func() {
114 | if err := dbpool.Close(); err != nil {
115 | t.Error(err)
116 | }
117 | }()
118 |
119 | webclient, err := New(dbpool, filer, ts.Client(), nil)
120 | if err != nil {
121 | t.Fatal(err)
122 | }
123 | defer webclient.Shutdown(context.Background())
124 |
125 | // Concurrent cache filling.
126 | wg := new(sync.WaitGroup)
127 | for i := 0; i < 100; i++ {
128 | i := i
129 | wg.Add(1)
130 | go func() {
131 | defer wg.Done()
132 |
133 | path := fmt.Sprintf("/file%d", i)
134 | res, err := webclient.Do(newReq(path))
135 | if err != nil {
136 | t.Fatal(err)
137 | }
138 | body, err := ioutil.ReadAll(res.Body)
139 | res.Body.Close()
140 | if err != nil {
141 | t.Fatal(err)
142 | }
143 | if got, want := string(body), "contentof:"+path; got != want {
144 | t.Errorf("got %q, want %q", got, want)
145 | }
146 | }()
147 | }
148 | wg.Wait()
149 |
150 | // Concurrent cache hits.
151 | wg = new(sync.WaitGroup)
152 | for i := 0; i < 100; i++ {
153 | i := i
154 | wg.Add(1)
155 | go func() {
156 | defer wg.Done()
157 |
158 | path := fmt.Sprintf("/file%d", i)
159 | res, err := webclient.Do(newReq(path))
160 | if err != nil {
161 | t.Fatal(err)
162 | }
163 | body, err := ioutil.ReadAll(res.Body)
164 | res.Body.Close()
165 | if err != nil {
166 | t.Fatal(err)
167 | }
168 | if got, want := string(body), "contentof:"+path; got != want {
169 | t.Errorf("got %q, want %q", got, want)
170 | }
171 | }()
172 | }
173 | wg.Wait()
174 | }
175 |
--------------------------------------------------------------------------------
/testdata/msg3.eml:
--------------------------------------------------------------------------------
1 | From: joe@spilled.ink
2 | To: crawshaw@spilled.ink
3 | MIME-Version: 1.0
4 | Content-Type: multipart/alternative; boundary="b2"
5 |
6 | --b2
7 | Content-Type: text/plain; charset="utf-8"
8 |
9 | Plain text.
10 | --b2
11 | Content-Type: text/html; charset="utf-8"
12 |
13 | Rich text.
14 | --b2
15 | Content-Type: text/rich; charset="utf-8"
16 |
17 | *Rich* text. Will get compressed.
18 | *Rich* text. Will get compressed.
19 | *Rich* text. Will get compressed.
20 | *Rich* text. Will get compressed.
21 | --b2--
22 |
23 |
--------------------------------------------------------------------------------
/testdata/msg4.eml:
--------------------------------------------------------------------------------
1 | To: david@zentus.com
2 | Subject: Hello
3 | From: joe@spilled.ink
4 | Date: Fri, 13 Jul 2018 16:39:01 -0000
5 | MIME-Version: 1.0
6 | Content-Type: text/plain; charset="utf-8"
7 | Content-Transfer-Encoding: quoted-printable
8 |
9 | Hello,
10 |
11 | This is a very long line that is broken using the venerable quoted-printabl=
12 | e encoding format. By being over 120 characters when it is re-encoded b=
13 | y the msgbuilder package it will be encoded as quoted-printable.
14 |
15 |
--------------------------------------------------------------------------------
/third_party/dns/AUTHORS:
--------------------------------------------------------------------------------
1 | Miek Gieben
2 |
--------------------------------------------------------------------------------
/third_party/dns/CONTRIBUTORS:
--------------------------------------------------------------------------------
1 | Alex A. Skinner
2 | Andrew Tunnell-Jones
3 | Ask Bjørn Hansen
4 | Dave Cheney
5 | Dusty Wilson
6 | Marek Majkowski
7 | Peter van Dijk
8 | Omri Bahumi
9 | Alex Sergeyev
10 | James Hartig
11 |
--------------------------------------------------------------------------------
/third_party/dns/COPYRIGHT:
--------------------------------------------------------------------------------
1 | Copyright 2009 The Go Authors. All rights reserved. Use of this source code
2 | is governed by a BSD-style license that can be found in the LICENSE file.
3 | Extensions of the original work are copyright (c) 2011 Miek Gieben
4 |
5 | Copyright 2011 Miek Gieben. All rights reserved. Use of this source code is
6 | governed by a BSD-style license that can be found in the LICENSE file.
7 |
8 | Copyright 2014 CloudFlare. All rights reserved. Use of this source code is
9 | governed by a BSD-style license that can be found in the LICENSE file.
10 |
--------------------------------------------------------------------------------
/third_party/dns/LICENSE:
--------------------------------------------------------------------------------
1 | Extensions of the original work are copyright (c) 2011 Miek Gieben
2 |
3 | As this is fork of the official Go code the same license applies:
4 |
5 | Copyright (c) 2009 The Go Authors. All rights reserved.
6 |
7 | Redistribution and use in source and binary forms, with or without
8 | modification, are permitted provided that the following conditions are
9 | met:
10 |
11 | * Redistributions of source code must retain the above copyright
12 | notice, this list of conditions and the following disclaimer.
13 | * Redistributions in binary form must reproduce the above
14 | copyright notice, this list of conditions and the following disclaimer
15 | in the documentation and/or other materials provided with the
16 | distribution.
17 | * Neither the name of Google Inc. nor the names of its
18 | contributors may be used to endorse or promote products derived from
19 | this software without specific prior written permission.
20 |
21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
24 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
25 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
26 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
27 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
28 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
29 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32 |
33 |
--------------------------------------------------------------------------------
/third_party/dns/README:
--------------------------------------------------------------------------------
1 | Fork of github.com/miekg/dns at 337216f9a77420e4401c8d32fa485fa3c0797440.
2 | Some parts removed.
3 |
--------------------------------------------------------------------------------
/third_party/dns/acceptfunc.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | // MsgAcceptFunc is used early in the server code to accept or reject a message with RcodeFormatError.
4 | // It returns a MsgAcceptAction to indicate what should happen with the message.
5 | type MsgAcceptFunc func(dh Header) MsgAcceptAction
6 |
7 | // DefaultMsgAcceptFunc checks the request and will reject if:
8 | //
9 | // * isn't a request (don't respond in that case).
10 | // * opcode isn't OpcodeQuery or OpcodeNotify
11 | // * Zero bit isn't zero
12 | // * has more than 1 question in the question section
13 | // * has more than 1 RR in the Answer section
14 | // * has more than 0 RRs in the Authority section
15 | // * has more than 2 RRs in the Additional section
16 | var DefaultMsgAcceptFunc MsgAcceptFunc = defaultMsgAcceptFunc
17 |
18 | // MsgAcceptAction represents the action to be taken.
19 | type MsgAcceptAction int
20 |
21 | const (
22 | MsgAccept MsgAcceptAction = iota // Accept the message
23 | MsgReject // Reject the message with a RcodeFormatError
24 | MsgIgnore // Ignore the error and send nothing back.
25 | )
26 |
27 | func defaultMsgAcceptFunc(dh Header) MsgAcceptAction {
28 | if isResponse := dh.Bits&_QR != 0; isResponse {
29 | return MsgIgnore
30 | }
31 |
32 | // Don't allow dynamic updates, because then the sections can contain a whole bunch of RRs.
33 | opcode := int(dh.Bits>>11) & 0xF
34 | if opcode != OpcodeQuery && opcode != OpcodeNotify {
35 | return MsgReject
36 | }
37 |
38 | if isZero := dh.Bits&_Z != 0; isZero {
39 | return MsgReject
40 | }
41 | if dh.Qdcount != 1 {
42 | return MsgReject
43 | }
44 | // NOTIFY requests can have a SOA in the ANSWER section. See RFC 1996 Section 3.7 and 3.11.
45 | if dh.Ancount > 1 {
46 | return MsgReject
47 | }
48 | // IXFR request could have one SOA RR in the NS section. See RFC 1995, section 3.
49 | if dh.Nscount > 1 {
50 | return MsgReject
51 | }
52 | if dh.Arcount > 2 {
53 | return MsgReject
54 | }
55 | return MsgAccept
56 | }
57 |
--------------------------------------------------------------------------------
/third_party/dns/client.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import "time"
4 |
5 | const (
6 | dnsTimeout time.Duration = 2 * time.Second
7 | tcpIdleTimeout time.Duration = 8 * time.Second
8 | )
9 |
--------------------------------------------------------------------------------
/third_party/dns/dns.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import "strconv"
4 |
5 | const (
6 | year68 = 1 << 31 // For RFC1982 (Serial Arithmetic) calculations in 32 bits.
7 | defaultTtl = 3600 // Default internal TTL.
8 |
9 | // DefaultMsgSize is the standard default for messages larger than 512 bytes.
10 | DefaultMsgSize = 4096
11 | // MinMsgSize is the minimal size of a DNS packet.
12 | MinMsgSize = 512
13 | // MaxMsgSize is the largest possible DNS packet.
14 | MaxMsgSize = 65535
15 | )
16 |
17 | // Error represents a DNS error.
18 | type Error struct{ err string }
19 |
20 | func (e *Error) Error() string {
21 | if e == nil {
22 | return "dns: "
23 | }
24 | return "dns: " + e.err
25 | }
26 |
27 | // An RR represents a resource record.
28 | type RR interface {
29 | // Header returns the header of an resource record. The header contains
30 | // everything up to the rdata.
31 | Header() *RR_Header
32 | // String returns the text representation of the resource record.
33 | String() string
34 |
35 | // copy returns a copy of the RR
36 | copy() RR
37 |
38 | // len returns the length (in octets) of the compressed or uncompressed RR in wire format.
39 | //
40 | // If compression is nil, the uncompressed size will be returned, otherwise the compressed
41 | // size will be returned and domain names will be added to the map for future compression.
42 | len(off int, compression map[string]struct{}) int
43 |
44 | // pack packs the records RDATA into wire format. The header will
45 | // already have been packed into msg.
46 | pack(msg []byte, off int, compression compressionMap, compress bool) (off1 int, err error)
47 |
48 | // unpack unpacks an RR from wire format.
49 | //
50 | // This will only be called on a new and empty RR type with only the header populated. It
51 | // will only be called if the record's RDATA is non-empty.
52 | unpack(msg []byte, off int) (off1 int, err error)
53 |
54 | // parse parses an RR from zone file format.
55 | //
56 | // This will only be called on a new and empty RR type with only the header populated.
57 | parse(c *zlexer, origin, file string) *ParseError
58 |
59 | // isDuplicate returns whether the two RRs are duplicates.
60 | isDuplicate(r2 RR) bool
61 | }
62 |
63 | // RR_Header is the header all DNS resource records share.
64 | type RR_Header struct {
65 | Name string `dns:"cdomain-name"`
66 | Rrtype uint16
67 | Class uint16
68 | Ttl uint32
69 | Rdlength uint16 // Length of data after header.
70 | }
71 |
72 | // Header returns itself. This is here to make RR_Header implements the RR interface.
73 | func (h *RR_Header) Header() *RR_Header { return h }
74 |
75 | // Just to implement the RR interface.
76 | func (h *RR_Header) copy() RR { return nil }
77 |
78 | func (h *RR_Header) String() string {
79 | var s string
80 |
81 | if h.Rrtype == TypeOPT {
82 | s = ";"
83 | // and maybe other things
84 | }
85 |
86 | s += sprintName(h.Name) + "\t"
87 | s += strconv.FormatInt(int64(h.Ttl), 10) + "\t"
88 | s += Class(h.Class).String() + "\t"
89 | s += Type(h.Rrtype).String() + "\t"
90 | return s
91 | }
92 |
93 | func (h *RR_Header) len(off int, compression map[string]struct{}) int {
94 | l := domainNameLen(h.Name, off, compression, true)
95 | l += 10 // rrtype(2) + class(2) + ttl(4) + rdlength(2)
96 | return l
97 | }
98 |
99 | func (h *RR_Header) pack(msg []byte, off int, compression compressionMap, compress bool) (off1 int, err error) {
100 | // RR_Header has no RDATA to pack.
101 | return off, nil
102 | }
103 |
104 | func (h *RR_Header) unpack(msg []byte, off int) (int, error) {
105 | panic("dns: internal error: unpack should never be called on RR_Header")
106 | }
107 |
108 | func (h *RR_Header) parse(c *zlexer, origin, file string) *ParseError {
109 | panic("dns: internal error: parse should never be called on RR_Header")
110 | }
111 |
112 | // ToRFC3597 converts a known RR to the unknown RR representation from RFC 3597.
113 | func (rr *RFC3597) ToRFC3597(r RR) error {
114 | buf := make([]byte, Len(r)*2)
115 | headerEnd, off, err := packRR(r, buf, 0, compressionMap{}, false)
116 | if err != nil {
117 | return err
118 | }
119 | buf = buf[:off]
120 |
121 | *rr = RFC3597{Hdr: *r.Header()}
122 | rr.Hdr.Rdlength = uint16(off - headerEnd)
123 |
124 | if noRdata(rr.Hdr) {
125 | return nil
126 | }
127 |
128 | _, err = rr.unpack(buf, headerEnd)
129 | if err != nil {
130 | return err
131 | }
132 |
133 | return nil
134 | }
135 |
--------------------------------------------------------------------------------
/third_party/dns/duplicate.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | //go:generate go run duplicate_generate.go
4 |
5 | // IsDuplicate checks of r1 and r2 are duplicates of each other, excluding the TTL.
6 | // So this means the header data is equal *and* the RDATA is the same. Return true
7 | // is so, otherwise false.
8 | // It's is a protocol violation to have identical RRs in a message.
9 | func IsDuplicate(r1, r2 RR) bool {
10 | // Check whether the record header is identical.
11 | if !r1.Header().isDuplicate(r2.Header()) {
12 | return false
13 | }
14 |
15 | // Check whether the RDATA is identical.
16 | return r1.isDuplicate(r2)
17 | }
18 |
19 | func (r1 *RR_Header) isDuplicate(_r2 RR) bool {
20 | r2, ok := _r2.(*RR_Header)
21 | if !ok {
22 | return false
23 | }
24 | if r1.Class != r2.Class {
25 | return false
26 | }
27 | if r1.Rrtype != r2.Rrtype {
28 | return false
29 | }
30 | if !isDuplicateName(r1.Name, r2.Name) {
31 | return false
32 | }
33 | // ignore TTL
34 | return true
35 | }
36 |
37 | // isDuplicateName checks if the domain names s1 and s2 are equal.
38 | func isDuplicateName(s1, s2 string) bool { return equal(s1, s2) }
39 |
--------------------------------------------------------------------------------
/third_party/dns/duplicate_generate.go:
--------------------------------------------------------------------------------
1 | //+build ignore
2 |
3 | // types_generate.go is meant to run with go generate. It will use
4 | // go/{importer,types} to track down all the RR struct types. Then for each type
5 | // it will generate conversion tables (TypeToRR and TypeToString) and banal
6 | // methods (len, Header, copy) based on the struct tags. The generated source is
7 | // written to ztypes.go, and is meant to be checked into git.
8 | package main
9 |
10 | import (
11 | "bytes"
12 | "fmt"
13 | "go/format"
14 | "go/importer"
15 | "go/types"
16 | "log"
17 | "os"
18 | )
19 |
20 | var packageHdr = `
21 | // Code generated by "go run duplicate_generate.go"; DO NOT EDIT.
22 |
23 | package dns
24 |
25 | `
26 |
27 | func getTypeStruct(t types.Type, scope *types.Scope) (*types.Struct, bool) {
28 | st, ok := t.Underlying().(*types.Struct)
29 | if !ok {
30 | return nil, false
31 | }
32 | if st.Field(0).Type() == scope.Lookup("RR_Header").Type() {
33 | return st, false
34 | }
35 | if st.Field(0).Anonymous() {
36 | st, _ := getTypeStruct(st.Field(0).Type(), scope)
37 | return st, true
38 | }
39 | return nil, false
40 | }
41 |
42 | func main() {
43 | // Import and type-check the package
44 | pkg, err := importer.Default().Import("spilled.ink/third_party/dns")
45 | fatalIfErr(err)
46 | scope := pkg.Scope()
47 |
48 | // Collect actual types (*X)
49 | var namedTypes []string
50 | for _, name := range scope.Names() {
51 | o := scope.Lookup(name)
52 | if o == nil || !o.Exported() {
53 | continue
54 | }
55 |
56 | if st, _ := getTypeStruct(o.Type(), scope); st == nil {
57 | continue
58 | }
59 |
60 | if name == "PrivateRR" || name == "OPT" {
61 | continue
62 | }
63 |
64 | namedTypes = append(namedTypes, o.Name())
65 | }
66 |
67 | b := &bytes.Buffer{}
68 | b.WriteString(packageHdr)
69 |
70 | // Generate the duplicate check for each type.
71 | fmt.Fprint(b, "// isDuplicate() functions\n\n")
72 | for _, name := range namedTypes {
73 |
74 | o := scope.Lookup(name)
75 | st, isEmbedded := getTypeStruct(o.Type(), scope)
76 | if isEmbedded {
77 | continue
78 | }
79 | fmt.Fprintf(b, "func (r1 *%s) isDuplicate(_r2 RR) bool {\n", name)
80 | fmt.Fprintf(b, "r2, ok := _r2.(*%s)\n", name)
81 | fmt.Fprint(b, "if !ok { return false }\n")
82 | fmt.Fprint(b, "_ = r2\n")
83 | for i := 1; i < st.NumFields(); i++ {
84 | field := st.Field(i).Name()
85 | o2 := func(s string) { fmt.Fprintf(b, s+"\n", field, field) }
86 | o3 := func(s string) { fmt.Fprintf(b, s+"\n", field, field, field) }
87 |
88 | // For some reason, a and aaaa don't pop up as *types.Slice here (mostly like because the are
89 | // *indirectly* defined as a slice in the net package).
90 | if _, ok := st.Field(i).Type().(*types.Slice); ok {
91 | o2("if len(r1.%s) != len(r2.%s) {\nreturn false\n}")
92 |
93 | if st.Tag(i) == `dns:"cdomain-name"` || st.Tag(i) == `dns:"domain-name"` {
94 | o3(`for i := 0; i < len(r1.%s); i++ {
95 | if !isDuplicateName(r1.%s[i], r2.%s[i]) {
96 | return false
97 | }
98 | }`)
99 |
100 | continue
101 | }
102 |
103 | o3(`for i := 0; i < len(r1.%s); i++ {
104 | if r1.%s[i] != r2.%s[i] {
105 | return false
106 | }
107 | }`)
108 |
109 | continue
110 | }
111 |
112 | switch st.Tag(i) {
113 | case `dns:"-"`:
114 | // ignored
115 | case `dns:"a"`, `dns:"aaaa"`:
116 | o2("if !r1.%s.Equal(r2.%s) {\nreturn false\n}")
117 | case `dns:"cdomain-name"`, `dns:"domain-name"`:
118 | o2("if !isDuplicateName(r1.%s, r2.%s) {\nreturn false\n}")
119 | default:
120 | o2("if r1.%s != r2.%s {\nreturn false\n}")
121 | }
122 | }
123 | fmt.Fprintf(b, "return true\n}\n\n")
124 | }
125 |
126 | // gofmt
127 | res, err := format.Source(b.Bytes())
128 | if err != nil {
129 | b.WriteTo(os.Stderr)
130 | log.Fatal(err)
131 | }
132 |
133 | // write result
134 | f, err := os.Create("zduplicate.go")
135 | fatalIfErr(err)
136 | defer f.Close()
137 | f.Write(res)
138 | }
139 |
140 | func fatalIfErr(err error) {
141 | if err != nil {
142 | log.Fatal(err)
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/third_party/dns/duplicate_test.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import "testing"
4 |
5 | func TestDuplicateA(t *testing.T) {
6 | a1, _ := NewRR("www.example.org. 2700 IN A 127.0.0.1")
7 | a2, _ := NewRR("www.example.org. IN A 127.0.0.1")
8 | if !IsDuplicate(a1, a2) {
9 | t.Errorf("expected %s/%s to be duplicates, but got false", a1.String(), a2.String())
10 | }
11 |
12 | a2, _ = NewRR("www.example.org. IN A 127.0.0.2")
13 | if IsDuplicate(a1, a2) {
14 | t.Errorf("expected %s/%s not to be duplicates, but got true", a1.String(), a2.String())
15 | }
16 | }
17 |
18 | func TestDuplicateTXT(t *testing.T) {
19 | a1, _ := NewRR("www.example.org. IN TXT \"aa\"")
20 | a2, _ := NewRR("www.example.org. IN TXT \"aa\"")
21 |
22 | if !IsDuplicate(a1, a2) {
23 | t.Errorf("expected %s/%s to be duplicates, but got false", a1.String(), a2.String())
24 | }
25 |
26 | a2, _ = NewRR("www.example.org. IN TXT \"aa\" \"bb\"")
27 | if IsDuplicate(a1, a2) {
28 | t.Errorf("expected %s/%s not to be duplicates, but got true", a1.String(), a2.String())
29 | }
30 |
31 | a1, _ = NewRR("www.example.org. IN TXT \"aa\" \"bc\"")
32 | if IsDuplicate(a1, a2) {
33 | t.Errorf("expected %s/%s not to be duplicates, but got true", a1.String(), a2.String())
34 | }
35 | }
36 |
37 | func TestDuplicateOwner(t *testing.T) {
38 | a1, _ := NewRR("www.example.org. IN A 127.0.0.1")
39 | a2, _ := NewRR("www.example.org. IN A 127.0.0.1")
40 | if !IsDuplicate(a1, a2) {
41 | t.Errorf("expected %s/%s to be duplicates, but got false", a1.String(), a2.String())
42 | }
43 |
44 | a2, _ = NewRR("WWw.exaMPle.org. IN A 127.0.0.2")
45 | if IsDuplicate(a1, a2) {
46 | t.Errorf("expected %s/%s to be duplicates, but got false", a1.String(), a2.String())
47 | }
48 | }
49 |
50 | func TestDuplicateDomain(t *testing.T) {
51 | a1, _ := NewRR("www.example.org. IN CNAME example.org.")
52 | a2, _ := NewRR("www.example.org. IN CNAME example.org.")
53 | if !IsDuplicate(a1, a2) {
54 | t.Errorf("expected %s/%s to be duplicates, but got false", a1.String(), a2.String())
55 | }
56 |
57 | a2, _ = NewRR("www.example.org. IN CNAME exAMPLe.oRG.")
58 | if !IsDuplicate(a1, a2) {
59 | t.Errorf("expected %s/%s to be duplicates, but got false", a1.String(), a2.String())
60 | }
61 | }
62 |
63 | func TestDuplicateWrongRrtype(t *testing.T) {
64 | // Test that IsDuplicate won't panic for a record that's lying about
65 | // it's Rrtype.
66 |
67 | r1 := &A{Hdr: RR_Header{Rrtype: TypeA}}
68 | r2 := &AAAA{Hdr: RR_Header{Rrtype: TypeA}}
69 | if IsDuplicate(r1, r2) {
70 | t.Errorf("expected %s/%s not to be duplicates, but got true", r1.String(), r2.String())
71 | }
72 |
73 | r3 := &AAAA{Hdr: RR_Header{Rrtype: TypeA}}
74 | r4 := &A{Hdr: RR_Header{Rrtype: TypeA}}
75 | if IsDuplicate(r3, r4) {
76 | t.Errorf("expected %s/%s not to be duplicates, but got true", r3.String(), r4.String())
77 | }
78 |
79 | r5 := &AAAA{Hdr: RR_Header{Rrtype: TypeA}}
80 | r6 := &AAAA{Hdr: RR_Header{Rrtype: TypeA}}
81 | if !IsDuplicate(r5, r6) {
82 | t.Errorf("expected %s/%s to be duplicates, but got false", r5.String(), r6.String())
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/third_party/dns/dyn_test.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | // Find better solution
4 |
--------------------------------------------------------------------------------
/third_party/dns/generate.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io"
7 | "strconv"
8 | "strings"
9 | )
10 |
11 | // Parse the $GENERATE statement as used in BIND9 zones.
12 | // See http://www.zytrax.com/books/dns/ch8/generate.html for instance.
13 | // We are called after '$GENERATE '. After which we expect:
14 | // * the range (12-24/2)
15 | // * lhs (ownername)
16 | // * [[ttl][class]]
17 | // * type
18 | // * rhs (rdata)
19 | // But we are lazy here, only the range is parsed *all* occurrences
20 | // of $ after that are interpreted.
21 | func (zp *ZoneParser) generate(l lex) (RR, bool) {
22 | token := l.token
23 | step := 1
24 | if i := strings.IndexByte(token, '/'); i >= 0 {
25 | if i+1 == len(token) {
26 | return zp.setParseError("bad step in $GENERATE range", l)
27 | }
28 |
29 | s, err := strconv.Atoi(token[i+1:])
30 | if err != nil || s <= 0 {
31 | return zp.setParseError("bad step in $GENERATE range", l)
32 | }
33 |
34 | step = s
35 | token = token[:i]
36 | }
37 |
38 | sx := strings.SplitN(token, "-", 2)
39 | if len(sx) != 2 {
40 | return zp.setParseError("bad start-stop in $GENERATE range", l)
41 | }
42 |
43 | start, err := strconv.Atoi(sx[0])
44 | if err != nil {
45 | return zp.setParseError("bad start in $GENERATE range", l)
46 | }
47 |
48 | end, err := strconv.Atoi(sx[1])
49 | if err != nil {
50 | return zp.setParseError("bad stop in $GENERATE range", l)
51 | }
52 | if end < 0 || start < 0 || end < start {
53 | return zp.setParseError("bad range in $GENERATE range", l)
54 | }
55 |
56 | zp.c.Next() // _BLANK
57 |
58 | // Create a complete new string, which we then parse again.
59 | var s string
60 | for l, ok := zp.c.Next(); ok; l, ok = zp.c.Next() {
61 | if l.err {
62 | return zp.setParseError("bad data in $GENERATE directive", l)
63 | }
64 | if l.value == zNewline {
65 | break
66 | }
67 |
68 | s += l.token
69 | }
70 |
71 | r := &generateReader{
72 | s: s,
73 |
74 | cur: start,
75 | start: start,
76 | end: end,
77 | step: step,
78 |
79 | file: zp.file,
80 | lex: &l,
81 | }
82 | zp.sub = NewZoneParser(r, zp.origin, zp.file)
83 | zp.sub.includeDepth, zp.sub.includeAllowed = zp.includeDepth, zp.includeAllowed
84 | zp.sub.SetDefaultTTL(defaultTtl)
85 | return zp.subNext()
86 | }
87 |
88 | type generateReader struct {
89 | s string
90 | si int
91 |
92 | cur int
93 | start int
94 | end int
95 | step int
96 |
97 | mod bytes.Buffer
98 |
99 | escape bool
100 |
101 | eof bool
102 |
103 | file string
104 | lex *lex
105 | }
106 |
107 | func (r *generateReader) parseError(msg string, end int) *ParseError {
108 | r.eof = true // Make errors sticky.
109 |
110 | l := *r.lex
111 | l.token = r.s[r.si-1 : end]
112 | l.column += r.si // l.column starts one zBLANK before r.s
113 |
114 | return &ParseError{r.file, msg, l}
115 | }
116 |
117 | func (r *generateReader) Read(p []byte) (int, error) {
118 | // NewZLexer, through NewZoneParser, should use ReadByte and
119 | // not end up here.
120 |
121 | panic("not implemented")
122 | }
123 |
124 | func (r *generateReader) ReadByte() (byte, error) {
125 | if r.eof {
126 | return 0, io.EOF
127 | }
128 | if r.mod.Len() > 0 {
129 | return r.mod.ReadByte()
130 | }
131 |
132 | if r.si >= len(r.s) {
133 | r.si = 0
134 | r.cur += r.step
135 |
136 | r.eof = r.cur > r.end || r.cur < 0
137 | return '\n', nil
138 | }
139 |
140 | si := r.si
141 | r.si++
142 |
143 | switch r.s[si] {
144 | case '\\':
145 | if r.escape {
146 | r.escape = false
147 | return '\\', nil
148 | }
149 |
150 | r.escape = true
151 | return r.ReadByte()
152 | case '$':
153 | if r.escape {
154 | r.escape = false
155 | return '$', nil
156 | }
157 |
158 | mod := "%d"
159 |
160 | if si >= len(r.s)-1 {
161 | // End of the string
162 | fmt.Fprintf(&r.mod, mod, r.cur)
163 | return r.mod.ReadByte()
164 | }
165 |
166 | if r.s[si+1] == '$' {
167 | r.si++
168 | return '$', nil
169 | }
170 |
171 | var offset int
172 |
173 | // Search for { and }
174 | if r.s[si+1] == '{' {
175 | // Modifier block
176 | sep := strings.Index(r.s[si+2:], "}")
177 | if sep < 0 {
178 | return 0, r.parseError("bad modifier in $GENERATE", len(r.s))
179 | }
180 |
181 | var errMsg string
182 | mod, offset, errMsg = modToPrintf(r.s[si+2 : si+2+sep])
183 | if errMsg != "" {
184 | return 0, r.parseError(errMsg, si+3+sep)
185 | }
186 | if r.start+offset < 0 || r.end+offset > 1<<31-1 {
187 | return 0, r.parseError("bad offset in $GENERATE", si+3+sep)
188 | }
189 |
190 | r.si += 2 + sep // Jump to it
191 | }
192 |
193 | fmt.Fprintf(&r.mod, mod, r.cur+offset)
194 | return r.mod.ReadByte()
195 | default:
196 | if r.escape { // Pretty useless here
197 | r.escape = false
198 | return r.ReadByte()
199 | }
200 |
201 | return r.s[si], nil
202 | }
203 | }
204 |
205 | // Convert a $GENERATE modifier 0,0,d to something Printf can deal with.
206 | func modToPrintf(s string) (string, int, string) {
207 | // Modifier is { offset [ ,width [ ,base ] ] } - provide default
208 | // values for optional width and type, if necessary.
209 | var offStr, widthStr, base string
210 | switch xs := strings.Split(s, ","); len(xs) {
211 | case 1:
212 | offStr, widthStr, base = xs[0], "0", "d"
213 | case 2:
214 | offStr, widthStr, base = xs[0], xs[1], "d"
215 | case 3:
216 | offStr, widthStr, base = xs[0], xs[1], xs[2]
217 | default:
218 | return "", 0, "bad modifier in $GENERATE"
219 | }
220 |
221 | switch base {
222 | case "o", "d", "x", "X":
223 | default:
224 | return "", 0, "bad base in $GENERATE"
225 | }
226 |
227 | offset, err := strconv.Atoi(offStr)
228 | if err != nil {
229 | return "", 0, "bad offset in $GENERATE"
230 | }
231 |
232 | width, err := strconv.Atoi(widthStr)
233 | if err != nil || width < 0 || width > 255 {
234 | return "", 0, "bad width in $GENERATE"
235 | }
236 |
237 | if width == 0 {
238 | return "%" + base, offset, ""
239 | }
240 |
241 | return "%0" + widthStr + base, offset, ""
242 | }
243 |
--------------------------------------------------------------------------------
/third_party/dns/issue_test.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | // Tests that solve that an specific issue.
4 |
5 | import (
6 | "strings"
7 | "testing"
8 | )
9 |
10 | func TestNSEC3MissingSalt(t *testing.T) {
11 | rr := testRR("ji6neoaepv8b5o6k4ev33abha8ht9fgc.example. NSEC3 1 1 12 aabbccdd K8UDEMVP1J2F7EG6JEBPS17VP3N8I58H")
12 | m := new(Msg)
13 | m.Answer = []RR{rr}
14 | mb, err := m.Pack()
15 | if err != nil {
16 | t.Fatalf("expected to pack message. err: %s", err)
17 | }
18 | if err := m.Unpack(mb); err != nil {
19 | t.Fatalf("expected to unpack message. missing salt? err: %s", err)
20 | }
21 | in := rr.(*NSEC3).Salt
22 | out := m.Answer[0].(*NSEC3).Salt
23 | if in != out {
24 | t.Fatalf("expected salts to match. packed: `%s`. returned: `%s`", in, out)
25 | }
26 | }
27 |
28 | func TestNSEC3MixedNextDomain(t *testing.T) {
29 | rr := testRR("ji6neoaepv8b5o6k4ev33abha8ht9fgc.example. NSEC3 1 1 12 - k8udemvp1j2f7eg6jebps17vp3n8i58h")
30 | m := new(Msg)
31 | m.Answer = []RR{rr}
32 | mb, err := m.Pack()
33 | if err != nil {
34 | t.Fatalf("expected to pack message. err: %s", err)
35 | }
36 | if err := m.Unpack(mb); err != nil {
37 | t.Fatalf("expected to unpack message. err: %s", err)
38 | }
39 | in := strings.ToUpper(rr.(*NSEC3).NextDomain)
40 | out := m.Answer[0].(*NSEC3).NextDomain
41 | if in != out {
42 | t.Fatalf("expected round trip to produce NextDomain `%s`, instead `%s`", in, out)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/third_party/dns/labels.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | // Holds a bunch of helper functions for dealing with labels.
4 |
5 | // SplitDomainName splits a name string into it's labels.
6 | // www.miek.nl. returns []string{"www", "miek", "nl"}
7 | // .www.miek.nl. returns []string{"", "www", "miek", "nl"},
8 | // The root label (.) returns nil. Note that using
9 | // strings.Split(s) will work in most cases, but does not handle
10 | // escaped dots (\.) for instance.
11 | // s must be a syntactically valid domain name, see IsDomainName.
12 | func SplitDomainName(s string) (labels []string) {
13 | if len(s) == 0 {
14 | return nil
15 | }
16 | fqdnEnd := 0 // offset of the final '.' or the length of the name
17 | idx := Split(s)
18 | begin := 0
19 | if IsFqdn(s) {
20 | fqdnEnd = len(s) - 1
21 | } else {
22 | fqdnEnd = len(s)
23 | }
24 |
25 | switch len(idx) {
26 | case 0:
27 | return nil
28 | case 1:
29 | // no-op
30 | default:
31 | end := 0
32 | for i := 1; i < len(idx); i++ {
33 | end = idx[i]
34 | labels = append(labels, s[begin:end-1])
35 | begin = end
36 | }
37 | }
38 |
39 | return append(labels, s[begin:fqdnEnd])
40 | }
41 |
42 | // CompareDomainName compares the names s1 and s2 and
43 | // returns how many labels they have in common starting from the *right*.
44 | // The comparison stops at the first inequality. The names are downcased
45 | // before the comparison.
46 | //
47 | // www.miek.nl. and miek.nl. have two labels in common: miek and nl
48 | // www.miek.nl. and www.bla.nl. have one label in common: nl
49 | //
50 | // s1 and s2 must be syntactically valid domain names.
51 | func CompareDomainName(s1, s2 string) (n int) {
52 | // the first check: root label
53 | if s1 == "." || s2 == "." {
54 | return 0
55 | }
56 |
57 | l1 := Split(s1)
58 | l2 := Split(s2)
59 |
60 | j1 := len(l1) - 1 // end
61 | i1 := len(l1) - 2 // start
62 | j2 := len(l2) - 1
63 | i2 := len(l2) - 2
64 | // the second check can be done here: last/only label
65 | // before we fall through into the for-loop below
66 | if equal(s1[l1[j1]:], s2[l2[j2]:]) {
67 | n++
68 | } else {
69 | return
70 | }
71 | for {
72 | if i1 < 0 || i2 < 0 {
73 | break
74 | }
75 | if equal(s1[l1[i1]:l1[j1]], s2[l2[i2]:l2[j2]]) {
76 | n++
77 | } else {
78 | break
79 | }
80 | j1--
81 | i1--
82 | j2--
83 | i2--
84 | }
85 | return
86 | }
87 |
88 | // CountLabel counts the the number of labels in the string s.
89 | // s must be a syntactically valid domain name.
90 | func CountLabel(s string) (labels int) {
91 | if s == "." {
92 | return
93 | }
94 | off := 0
95 | end := false
96 | for {
97 | off, end = NextLabel(s, off)
98 | labels++
99 | if end {
100 | return
101 | }
102 | }
103 | }
104 |
105 | // Split splits a name s into its label indexes.
106 | // www.miek.nl. returns []int{0, 4, 9}, www.miek.nl also returns []int{0, 4, 9}.
107 | // The root name (.) returns nil. Also see SplitDomainName.
108 | // s must be a syntactically valid domain name.
109 | func Split(s string) []int {
110 | if s == "." {
111 | return nil
112 | }
113 | idx := make([]int, 1, 3)
114 | off := 0
115 | end := false
116 |
117 | for {
118 | off, end = NextLabel(s, off)
119 | if end {
120 | return idx
121 | }
122 | idx = append(idx, off)
123 | }
124 | }
125 |
126 | // NextLabel returns the index of the start of the next label in the
127 | // string s starting at offset.
128 | // The bool end is true when the end of the string has been reached.
129 | // Also see PrevLabel.
130 | func NextLabel(s string, offset int) (i int, end bool) {
131 | quote := false
132 | for i = offset; i < len(s)-1; i++ {
133 | switch s[i] {
134 | case '\\':
135 | quote = !quote
136 | default:
137 | quote = false
138 | case '.':
139 | if quote {
140 | quote = !quote
141 | continue
142 | }
143 | return i + 1, false
144 | }
145 | }
146 | return i + 1, true
147 | }
148 |
149 | // PrevLabel returns the index of the label when starting from the right and
150 | // jumping n labels to the left.
151 | // The bool start is true when the start of the string has been overshot.
152 | // Also see NextLabel.
153 | func PrevLabel(s string, n int) (i int, start bool) {
154 | if n == 0 {
155 | return len(s), false
156 | }
157 | lab := Split(s)
158 | if lab == nil {
159 | return 0, true
160 | }
161 | if n > len(lab) {
162 | return 0, true
163 | }
164 | return lab[len(lab)-n], false
165 | }
166 |
167 | // equal compares a and b while ignoring case. It returns true when equal otherwise false.
168 | func equal(a, b string) bool {
169 | // might be lifted into API function.
170 | la := len(a)
171 | lb := len(b)
172 | if la != lb {
173 | return false
174 | }
175 |
176 | for i := la - 1; i >= 0; i-- {
177 | ai := a[i]
178 | bi := b[i]
179 | if ai >= 'A' && ai <= 'Z' {
180 | ai |= 'a' - 'A'
181 | }
182 | if bi >= 'A' && bi <= 'Z' {
183 | bi |= 'a' - 'A'
184 | }
185 | if ai != bi {
186 | return false
187 | }
188 | }
189 | return true
190 | }
191 |
--------------------------------------------------------------------------------
/third_party/dns/leak_test.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "runtime"
7 | "sort"
8 | "strings"
9 | "testing"
10 | "time"
11 | )
12 |
13 | // copied from net/http/main_test.go
14 |
15 | func interestingGoroutines() (gs []string) {
16 | buf := make([]byte, 2<<20)
17 | buf = buf[:runtime.Stack(buf, true)]
18 | for _, g := range strings.Split(string(buf), "\n\n") {
19 | sl := strings.SplitN(g, "\n", 2)
20 | if len(sl) != 2 {
21 | continue
22 | }
23 | stack := strings.TrimSpace(sl[1])
24 | if stack == "" ||
25 | strings.Contains(stack, "testing.(*M).before.func1") ||
26 | strings.Contains(stack, "os/signal.signal_recv") ||
27 | strings.Contains(stack, "created by net.startServer") ||
28 | strings.Contains(stack, "created by testing.RunTests") ||
29 | strings.Contains(stack, "closeWriteAndWait") ||
30 | strings.Contains(stack, "testing.Main(") ||
31 | strings.Contains(stack, "testing.(*T).Run(") ||
32 | // These only show up with GOTRACEBACK=2; Issue 5005 (comment 28)
33 | strings.Contains(stack, "runtime.goexit") ||
34 | strings.Contains(stack, "created by runtime.gc") ||
35 | strings.Contains(stack, "dns.interestingGoroutines") ||
36 | strings.Contains(stack, "runtime.MHeap_Scavenger") {
37 | continue
38 | }
39 | gs = append(gs, stack)
40 | }
41 | sort.Strings(gs)
42 | return
43 | }
44 |
45 | func goroutineLeaked() error {
46 | if testing.Short() {
47 | // Don't worry about goroutine leaks in -short mode or in
48 | // benchmark mode. Too distracting when there are false positives.
49 | return nil
50 | }
51 |
52 | var stackCount map[string]int
53 | for i := 0; i < 5; i++ {
54 | n := 0
55 | stackCount = make(map[string]int)
56 | gs := interestingGoroutines()
57 | for _, g := range gs {
58 | stackCount[g]++
59 | n++
60 | }
61 | if n == 0 {
62 | return nil
63 | }
64 | // Wait for goroutines to schedule and die off:
65 | time.Sleep(100 * time.Millisecond)
66 | }
67 | for stack, count := range stackCount {
68 | fmt.Fprintf(os.Stderr, "%d instances of:\n%s\n", count, stack)
69 | }
70 | return fmt.Errorf("too many goroutines running after dns test(s)")
71 | }
72 |
--------------------------------------------------------------------------------
/third_party/dns/listen_go111.go:
--------------------------------------------------------------------------------
1 | // +build go1.11
2 | // +build aix darwin dragonfly freebsd linux netbsd openbsd
3 |
4 | package dns
5 |
6 | import (
7 | "context"
8 | "net"
9 | "syscall"
10 |
11 | "golang.org/x/sys/unix"
12 | )
13 |
14 | const supportsReusePort = true
15 |
16 | func reuseportControl(network, address string, c syscall.RawConn) error {
17 | var opErr error
18 | err := c.Control(func(fd uintptr) {
19 | opErr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
20 | })
21 | if err != nil {
22 | return err
23 | }
24 |
25 | return opErr
26 | }
27 |
28 | func listenTCP(network, addr string, reuseport bool) (net.Listener, error) {
29 | var lc net.ListenConfig
30 | if reuseport {
31 | lc.Control = reuseportControl
32 | }
33 |
34 | return lc.Listen(context.Background(), network, addr)
35 | }
36 |
37 | func listenUDP(network, addr string, reuseport bool) (net.PacketConn, error) {
38 | var lc net.ListenConfig
39 | if reuseport {
40 | lc.Control = reuseportControl
41 | }
42 |
43 | return lc.ListenPacket(context.Background(), network, addr)
44 | }
45 |
--------------------------------------------------------------------------------
/third_party/dns/listen_go_not111.go:
--------------------------------------------------------------------------------
1 | // +build !go1.11 !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd
2 |
3 | package dns
4 |
5 | import "net"
6 |
7 | const supportsReusePort = false
8 |
9 | func listenTCP(network, addr string, reuseport bool) (net.Listener, error) {
10 | if reuseport {
11 | // TODO(tmthrgd): return an error?
12 | }
13 |
14 | return net.Listen(network, addr)
15 | }
16 |
17 | func listenUDP(network, addr string, reuseport bool) (net.PacketConn, error) {
18 | if reuseport {
19 | // TODO(tmthrgd): return an error?
20 | }
21 |
22 | return net.ListenPacket(network, addr)
23 | }
24 |
--------------------------------------------------------------------------------
/third_party/dns/msg_helpers_test.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import "testing"
4 |
5 | // TestPacketDataNsec tests generated using fuzz.go and with a message pack
6 | // containing the following bytes: 0000\x00\x00000000\x00\x002000000\x0060000\x00\x130000000000000000000"
7 | // That bytes sequence created the overflow error and further permutations of that sequence were able to trigger
8 | // the other code paths.
9 | func TestPackDataNsec(t *testing.T) {
10 | type args struct {
11 | bitmap []uint16
12 | msg []byte
13 | off int
14 | }
15 | tests := []struct {
16 | name string
17 | args args
18 | want int
19 | wantErr bool
20 | wantErrMsg string
21 | }{
22 | {
23 | name: "overflow",
24 | args: args{
25 | bitmap: []uint16{
26 | 8962, 8963, 8970, 8971, 8978, 8979,
27 | 8986, 8987, 8994, 8995, 9002, 9003,
28 | 9010, 9011, 9018, 9019, 9026, 9027,
29 | 9034, 9035, 9042, 9043, 9050, 9051,
30 | 9058, 9059, 9066,
31 | },
32 | msg: []byte{
33 | 48, 48, 48, 48, 0, 0, 0,
34 | 1, 0, 0, 0, 0, 0, 0, 50,
35 | 48, 48, 48, 48, 48, 48,
36 | 0, 54, 48, 48, 48, 48,
37 | 0, 19, 48, 48,
38 | },
39 | off: 48,
40 | },
41 | wantErr: true,
42 | wantErrMsg: "dns: overflow packing nsec",
43 | want: 31,
44 | },
45 | {
46 | name: "disordered nsec bits",
47 | args: args{
48 | bitmap: []uint16{
49 | 8962,
50 | 0,
51 | },
52 | msg: []byte{
53 | 48, 48, 48, 48, 0, 0, 0, 1, 0, 0, 0, 0,
54 | 0, 0, 50, 48, 48, 48, 48, 48, 48, 0, 54, 48,
55 | 48, 48, 48, 0, 19, 48, 48, 48, 48, 48, 48, 0,
56 | 0, 0, 1, 0, 0, 0, 0, 0, 0, 50, 48, 48,
57 | 48, 48, 48, 48, 0, 54, 48, 48, 48, 48, 0, 19,
58 | 48, 48, 48, 48, 48, 48, 0, 0, 0, 1, 0, 0,
59 | 0, 0, 0, 0, 50, 48, 48, 48, 48, 48, 48, 0,
60 | 54, 48, 48, 48, 48, 0, 19, 48, 48, 48, 48, 48,
61 | 48, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 50,
62 | 48, 48, 48, 48, 48, 48, 0, 54, 48, 48, 48, 48,
63 | 0, 19, 48, 48, 48, 48, 48, 48, 0, 0, 0, 1,
64 | 0, 0, 0, 0, 0, 0, 50, 48, 48, 48, 48, 48,
65 | 48, 0, 54, 48, 48, 48, 48, 0, 19, 48, 48,
66 | },
67 | off: 0,
68 | },
69 | wantErr: true,
70 | wantErrMsg: "dns: nsec bits out of order",
71 | want: 155,
72 | },
73 | {
74 | name: "simple message with only one window",
75 | args: args{
76 | bitmap: []uint16{
77 | 0,
78 | },
79 | msg: []byte{
80 | 48, 48, 48, 48, 0, 0,
81 | 0, 1, 0, 0, 0, 0,
82 | 0, 0, 50, 48, 48, 48,
83 | 48, 48, 48, 0, 54, 48,
84 | 48, 48, 48, 0, 19, 48, 48,
85 | },
86 | off: 0,
87 | },
88 | wantErr: false,
89 | want: 3,
90 | },
91 | }
92 | for _, tt := range tests {
93 | t.Run(tt.name, func(t *testing.T) {
94 | got, err := packDataNsec(tt.args.bitmap, tt.args.msg, tt.args.off)
95 | if (err != nil) != tt.wantErr {
96 | t.Errorf("packDataNsec() error = %v, wantErr %v", err, tt.wantErr)
97 | return
98 | }
99 | if err != nil && tt.wantErrMsg != err.Error() {
100 | t.Errorf("packDataNsec() error msg = %v, wantErrMsg %v", err.Error(), tt.wantErrMsg)
101 | return
102 | }
103 | if got != tt.want {
104 | t.Errorf("packDataNsec() = %v, want %v", got, tt.want)
105 | }
106 | })
107 | }
108 | }
109 |
110 | func TestUnpackString(t *testing.T) {
111 | msg := []byte("\x00abcdef\x0f\\\"ghi\x04mmm\x7f")
112 | msg[0] = byte(len(msg) - 1)
113 |
114 | got, _, err := unpackString(msg, 0)
115 | if err != nil {
116 | t.Fatal(err)
117 | }
118 |
119 | if want := `abcdef\015\\\"ghi\004mmm\127`; want != got {
120 | t.Errorf("expected %q, got %q", want, got)
121 | }
122 | }
123 |
124 | func BenchmarkUnpackString(b *testing.B) {
125 | msg := []byte("\x00abcdef\x0f\\\"ghi\x04mmm")
126 | msg[0] = byte(len(msg) - 1)
127 |
128 | for n := 0; n < b.N; n++ {
129 | got, _, err := unpackString(msg, 0)
130 | if err != nil {
131 | b.Fatal(err)
132 | }
133 |
134 | if want := `abcdef\015\\\"ghi\004mmm`; want != got {
135 | b.Errorf("expected %q, got %q", want, got)
136 | }
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/third_party/dns/reverse.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | // StringToType is the reverse of TypeToString, needed for string parsing.
4 | var StringToType = reverseInt16(TypeToString)
5 |
6 | // StringToClass is the reverse of ClassToString, needed for string parsing.
7 | var StringToClass = reverseInt16(ClassToString)
8 |
9 | // StringToOpcode is a map of opcodes to strings.
10 | var StringToOpcode = reverseInt(OpcodeToString)
11 |
12 | // StringToRcode is a map of rcodes to strings.
13 | var StringToRcode = reverseInt(RcodeToString)
14 |
15 | func init() {
16 | // Preserve previous NOTIMP typo, see github.com/miekg/dns/issues/733.
17 | StringToRcode["NOTIMPL"] = RcodeNotImplemented
18 | }
19 |
20 | // StringToCertType is the reverseof CertTypeToString.
21 | var StringToCertType = reverseInt16(CertTypeToString)
22 |
23 | // Reverse a map
24 | func reverseInt8(m map[uint8]string) map[string]uint8 {
25 | n := make(map[string]uint8, len(m))
26 | for u, s := range m {
27 | n[s] = u
28 | }
29 | return n
30 | }
31 |
32 | func reverseInt16(m map[uint16]string) map[string]uint16 {
33 | n := make(map[string]uint16, len(m))
34 | for u, s := range m {
35 | n[s] = u
36 | }
37 | return n
38 | }
39 |
40 | func reverseInt(m map[int]string) map[string]int {
41 | n := make(map[string]int, len(m))
42 | for u, s := range m {
43 | n[s] = u
44 | }
45 | return n
46 | }
47 |
--------------------------------------------------------------------------------
/third_party/dns/rr_test.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | // testRR is a helper that wraps a call to NewRR and panics if the error is non-nil.
4 | func testRR(s string) RR {
5 | r, err := NewRR(s)
6 | if err != nil {
7 | panic(err)
8 | }
9 |
10 | return r
11 | }
12 |
--------------------------------------------------------------------------------
/third_party/dns/serve_mux.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import (
4 | "strings"
5 | "sync"
6 | )
7 |
8 | // ServeMux is an DNS request multiplexer. It matches the zone name of
9 | // each incoming request against a list of registered patterns add calls
10 | // the handler for the pattern that most closely matches the zone name.
11 | //
12 | // ServeMux is DNSSEC aware, meaning that queries for the DS record are
13 | // redirected to the parent zone (if that is also registered), otherwise
14 | // the child gets the query.
15 | //
16 | // ServeMux is also safe for concurrent access from multiple goroutines.
17 | //
18 | // The zero ServeMux is empty and ready for use.
19 | type ServeMux struct {
20 | z map[string]Handler
21 | m sync.RWMutex
22 | }
23 |
24 | // NewServeMux allocates and returns a new ServeMux.
25 | func NewServeMux() *ServeMux {
26 | return new(ServeMux)
27 | }
28 |
29 | func (mux *ServeMux) match(q string, t uint16) Handler {
30 | mux.m.RLock()
31 | defer mux.m.RUnlock()
32 | if mux.z == nil {
33 | return nil
34 | }
35 |
36 | var handler Handler
37 |
38 | // TODO(tmthrgd): Once https://go-review.googlesource.com/c/go/+/137575
39 | // lands in a go release, replace the following with strings.ToLower.
40 | var sb strings.Builder
41 | for i := 0; i < len(q); i++ {
42 | c := q[i]
43 | if !(c >= 'A' && c <= 'Z') {
44 | continue
45 | }
46 |
47 | sb.Grow(len(q))
48 | sb.WriteString(q[:i])
49 |
50 | for ; i < len(q); i++ {
51 | c := q[i]
52 | if c >= 'A' && c <= 'Z' {
53 | c += 'a' - 'A'
54 | }
55 |
56 | sb.WriteByte(c)
57 | }
58 |
59 | q = sb.String()
60 | break
61 | }
62 |
63 | for off, end := 0, false; !end; off, end = NextLabel(q, off) {
64 | if h, ok := mux.z[q[off:]]; ok {
65 | if t != TypeDS {
66 | return h
67 | }
68 | // Continue for DS to see if we have a parent too, if so delegate to the parent
69 | handler = h
70 | }
71 | }
72 |
73 | // Wildcard match, if we have found nothing try the root zone as a last resort.
74 | if h, ok := mux.z["."]; ok {
75 | return h
76 | }
77 |
78 | return handler
79 | }
80 |
81 | // Handle adds a handler to the ServeMux for pattern.
82 | func (mux *ServeMux) Handle(pattern string, handler Handler) {
83 | if pattern == "" {
84 | panic("dns: invalid pattern " + pattern)
85 | }
86 | mux.m.Lock()
87 | if mux.z == nil {
88 | mux.z = make(map[string]Handler)
89 | }
90 | mux.z[Fqdn(pattern)] = handler
91 | mux.m.Unlock()
92 | }
93 |
94 | // HandleFunc adds a handler function to the ServeMux for pattern.
95 | func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Msg)) {
96 | mux.Handle(pattern, HandlerFunc(handler))
97 | }
98 |
99 | // HandleRemove deregisters the handler specific for pattern from the ServeMux.
100 | func (mux *ServeMux) HandleRemove(pattern string) {
101 | if pattern == "" {
102 | panic("dns: invalid pattern " + pattern)
103 | }
104 | mux.m.Lock()
105 | delete(mux.z, Fqdn(pattern))
106 | mux.m.Unlock()
107 | }
108 |
109 | // ServeDNS dispatches the request to the handler whose pattern most
110 | // closely matches the request message.
111 | //
112 | // ServeDNS is DNSSEC aware, meaning that queries for the DS record
113 | // are redirected to the parent zone (if that is also registered),
114 | // otherwise the child gets the query.
115 | //
116 | // If no handler is found, or there is no question, a standard SERVFAIL
117 | // message is returned
118 | func (mux *ServeMux) ServeDNS(w ResponseWriter, req *Msg) {
119 | var h Handler
120 | if len(req.Question) >= 1 { // allow more than one question
121 | h = mux.match(req.Question[0].Name, req.Question[0].Qtype)
122 | }
123 |
124 | if h != nil {
125 | h.ServeDNS(w, req)
126 | } else {
127 | HandleFailed(w, req)
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/third_party/dns/serve_mux_test.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import "testing"
4 |
5 | func TestDotAsCatchAllWildcard(t *testing.T) {
6 | mux := NewServeMux()
7 | mux.Handle(".", HandlerFunc(HelloServer))
8 | mux.Handle("example.com.", HandlerFunc(AnotherHelloServer))
9 |
10 | handler := mux.match("www.miek.nl.", TypeTXT)
11 | if handler == nil {
12 | t.Error("wildcard match failed")
13 | }
14 |
15 | handler = mux.match("www.example.com.", TypeTXT)
16 | if handler == nil {
17 | t.Error("example.com match failed")
18 | }
19 |
20 | handler = mux.match("a.www.example.com.", TypeTXT)
21 | if handler == nil {
22 | t.Error("a.www.example.com match failed")
23 | }
24 |
25 | handler = mux.match("boe.", TypeTXT)
26 | if handler == nil {
27 | t.Error("boe. match failed")
28 | }
29 | }
30 |
31 | func TestCaseFolding(t *testing.T) {
32 | mux := NewServeMux()
33 | mux.Handle("_udp.example.com.", HandlerFunc(HelloServer))
34 |
35 | handler := mux.match("_dns._udp.example.com.", TypeSRV)
36 | if handler == nil {
37 | t.Error("case sensitive characters folded")
38 | }
39 |
40 | handler = mux.match("_DNS._UDP.EXAMPLE.COM.", TypeSRV)
41 | if handler == nil {
42 | t.Error("case insensitive characters not folded")
43 | }
44 | }
45 |
46 | func TestRootServer(t *testing.T) {
47 | mux := NewServeMux()
48 | mux.Handle(".", HandlerFunc(HelloServer))
49 |
50 | handler := mux.match(".", TypeNS)
51 | if handler == nil {
52 | t.Error("root match failed")
53 | }
54 | }
55 |
56 | func BenchmarkMuxMatch(b *testing.B) {
57 | mux := NewServeMux()
58 | mux.Handle("_udp.example.com.", HandlerFunc(HelloServer))
59 |
60 | bench := func(q string) func(*testing.B) {
61 | return func(b *testing.B) {
62 | for n := 0; n < b.N; n++ {
63 | handler := mux.match(q, TypeSRV)
64 | if handler == nil {
65 | b.Fatal("couldn't find match")
66 | }
67 | }
68 | }
69 | }
70 | b.Run("lowercase", bench("_dns._udp.example.com."))
71 | b.Run("uppercase", bench("_DNS._UDP.EXAMPLE.COM."))
72 | }
73 |
--------------------------------------------------------------------------------
/third_party/dns/types_test.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestCmToM(t *testing.T) {
8 | s := cmToM(0, 0)
9 | if s != "0.00" {
10 | t.Error("0, 0")
11 | }
12 |
13 | s = cmToM(1, 0)
14 | if s != "0.01" {
15 | t.Error("1, 0")
16 | }
17 |
18 | s = cmToM(3, 1)
19 | if s != "0.30" {
20 | t.Error("3, 1")
21 | }
22 |
23 | s = cmToM(4, 2)
24 | if s != "4" {
25 | t.Error("4, 2")
26 | }
27 |
28 | s = cmToM(5, 3)
29 | if s != "50" {
30 | t.Error("5, 3")
31 | }
32 |
33 | s = cmToM(7, 5)
34 | if s != "7000" {
35 | t.Error("7, 5")
36 | }
37 |
38 | s = cmToM(9, 9)
39 | if s != "90000000" {
40 | t.Error("9, 9")
41 | }
42 | }
43 |
44 | func TestSplitN(t *testing.T) {
45 | xs := splitN("abc", 5)
46 | if len(xs) != 1 && xs[0] != "abc" {
47 | t.Errorf("failure to split abc")
48 | }
49 |
50 | s := ""
51 | for i := 0; i < 255; i++ {
52 | s += "a"
53 | }
54 |
55 | xs = splitN(s, 255)
56 | if len(xs) != 1 && xs[0] != s {
57 | t.Errorf("failure to split 255 char long string")
58 | }
59 |
60 | s += "b"
61 | xs = splitN(s, 255)
62 | if len(xs) != 2 || xs[1] != "b" {
63 | t.Errorf("failure to split 256 char long string: %d", len(xs))
64 | }
65 |
66 | // Make s longer
67 | for i := 0; i < 255; i++ {
68 | s += "a"
69 | }
70 | xs = splitN(s, 255)
71 | if len(xs) != 3 || xs[2] != "a" {
72 | t.Errorf("failure to split 510 char long string: %d", len(xs))
73 | }
74 | }
75 |
76 | func TestSprintName(t *testing.T) {
77 | got := sprintName("abc\\.def\007\"\127@\255\x05\xef\\")
78 |
79 | if want := "abc\\.def\\007\\\"W\\@\\173\\005\\239"; got != want {
80 | t.Errorf("expected %q, got %q", got, want)
81 | }
82 | }
83 |
84 | func TestSprintTxtOctet(t *testing.T) {
85 | got := sprintTxtOctet("abc\\.def\007\"\127@\255\x05\xef\\")
86 |
87 | if want := "\"abc\\.def\\007\"W@\\173\\005\\239\""; got != want {
88 | t.Errorf("expected %q, got %q", got, want)
89 | }
90 | }
91 |
92 | func TestSprintTxt(t *testing.T) {
93 | got := sprintTxt([]string{
94 | "abc\\.def\007\"\127@\255\x05\xef\\",
95 | "example.com",
96 | })
97 |
98 | if want := "\"abc.def\\007\\\"W@\\173\\005\\239\" \"example.com\""; got != want {
99 | t.Errorf("expected %q, got %q", got, want)
100 | }
101 | }
102 |
103 | func TestRPStringer(t *testing.T) {
104 | rp := &RP{
105 | Hdr: RR_Header{
106 | Name: "test.example.com.",
107 | Rrtype: TypeRP,
108 | Class: ClassINET,
109 | Ttl: 600,
110 | },
111 | Mbox: "\x05first.example.com.",
112 | Txt: "second.\x07example.com.",
113 | }
114 |
115 | const expected = "test.example.com.\t600\tIN\tRP\t\\005first.example.com. second.\\007example.com."
116 | if rp.String() != expected {
117 | t.Errorf("expected %v, got %v", expected, rp)
118 | }
119 |
120 | _, err := NewRR(rp.String())
121 | if err != nil {
122 | t.Fatalf("error parsing %q: %v", rp, err)
123 | }
124 | }
125 |
126 | func BenchmarkSprintName(b *testing.B) {
127 | for n := 0; n < b.N; n++ {
128 | got := sprintName("abc\\.def\007\"\127@\255\x05\xef\\")
129 |
130 | if want := "abc\\.def\\007\\\"W\\@\\173\\005\\239"; got != want {
131 | b.Fatalf("expected %q, got %q", got, want)
132 | }
133 | }
134 | }
135 |
136 | func BenchmarkSprintTxtOctet(b *testing.B) {
137 | for n := 0; n < b.N; n++ {
138 | got := sprintTxtOctet("abc\\.def\007\"\127@\255\x05\xef\\")
139 |
140 | if want := "\"abc\\.def\\007\"W@\\173\\005\\239\""; got != want {
141 | b.Fatalf("expected %q, got %q", got, want)
142 | }
143 | }
144 | }
145 |
146 | func BenchmarkSprintTxt(b *testing.B) {
147 | txt := []string{
148 | "abc\\.def\007\"\127@\255\x05\xef\\",
149 | "example.com",
150 | }
151 |
152 | for n := 0; n < b.N; n++ {
153 | got := sprintTxt(txt)
154 |
155 | if want := "\"abc.def\\007\\\"W@\\173\\005\\239\" \"example.com\""; got != want {
156 | b.Fatalf("expected %q, got %q", got, want)
157 | }
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/third_party/dns/udp.go:
--------------------------------------------------------------------------------
1 | // +build !windows
2 |
3 | package dns
4 |
5 | import (
6 | "net"
7 |
8 | "golang.org/x/net/ipv4"
9 | "golang.org/x/net/ipv6"
10 | )
11 |
12 | // This is the required size of the OOB buffer to pass to ReadMsgUDP.
13 | var udpOOBSize = func() int {
14 | // We can't know whether we'll get an IPv4 control message or an
15 | // IPv6 control message ahead of time. To get around this, we size
16 | // the buffer equal to the largest of the two.
17 |
18 | oob4 := ipv4.NewControlMessage(ipv4.FlagDst | ipv4.FlagInterface)
19 | oob6 := ipv6.NewControlMessage(ipv6.FlagDst | ipv6.FlagInterface)
20 |
21 | if len(oob4) > len(oob6) {
22 | return len(oob4)
23 | }
24 |
25 | return len(oob6)
26 | }()
27 |
28 | // SessionUDP holds the remote address and the associated
29 | // out-of-band data.
30 | type SessionUDP struct {
31 | raddr *net.UDPAddr
32 | context []byte
33 | }
34 |
35 | // RemoteAddr returns the remote network address.
36 | func (s *SessionUDP) RemoteAddr() net.Addr { return s.raddr }
37 |
38 | // ReadFromSessionUDP acts just like net.UDPConn.ReadFrom(), but returns a session object instead of a
39 | // net.UDPAddr.
40 | func ReadFromSessionUDP(conn *net.UDPConn, b []byte) (int, *SessionUDP, error) {
41 | oob := make([]byte, udpOOBSize)
42 | n, oobn, _, raddr, err := conn.ReadMsgUDP(b, oob)
43 | if err != nil {
44 | return n, nil, err
45 | }
46 | return n, &SessionUDP{raddr, oob[:oobn]}, err
47 | }
48 |
49 | // WriteToSessionUDP acts just like net.UDPConn.WriteTo(), but uses a *SessionUDP instead of a net.Addr.
50 | func WriteToSessionUDP(conn *net.UDPConn, b []byte, session *SessionUDP) (int, error) {
51 | oob := correctSource(session.context)
52 | n, _, err := conn.WriteMsgUDP(b, oob, session.raddr)
53 | return n, err
54 | }
55 |
56 | func setUDPSocketOptions(conn *net.UDPConn) error {
57 | // Try setting the flags for both families and ignore the errors unless they
58 | // both error.
59 | err6 := ipv6.NewPacketConn(conn).SetControlMessage(ipv6.FlagDst|ipv6.FlagInterface, true)
60 | err4 := ipv4.NewPacketConn(conn).SetControlMessage(ipv4.FlagDst|ipv4.FlagInterface, true)
61 | if err6 != nil && err4 != nil {
62 | return err4
63 | }
64 | return nil
65 | }
66 |
67 | // parseDstFromOOB takes oob data and returns the destination IP.
68 | func parseDstFromOOB(oob []byte) net.IP {
69 | // Start with IPv6 and then fallback to IPv4
70 | // TODO(fastest963): Figure out a way to prefer one or the other. Looking at
71 | // the lvl of the header for a 0 or 41 isn't cross-platform.
72 | cm6 := new(ipv6.ControlMessage)
73 | if cm6.Parse(oob) == nil && cm6.Dst != nil {
74 | return cm6.Dst
75 | }
76 | cm4 := new(ipv4.ControlMessage)
77 | if cm4.Parse(oob) == nil && cm4.Dst != nil {
78 | return cm4.Dst
79 | }
80 | return nil
81 | }
82 |
83 | // correctSource takes oob data and returns new oob data with the Src equal to the Dst
84 | func correctSource(oob []byte) []byte {
85 | dst := parseDstFromOOB(oob)
86 | if dst == nil {
87 | return nil
88 | }
89 | // If the dst is definitely an IPv6, then use ipv6's ControlMessage to
90 | // respond otherwise use ipv4's because ipv6's marshal ignores ipv4
91 | // addresses.
92 | if dst.To4() == nil {
93 | cm := new(ipv6.ControlMessage)
94 | cm.Src = dst
95 | oob = cm.Marshal()
96 | } else {
97 | cm := new(ipv4.ControlMessage)
98 | cm.Src = dst
99 | oob = cm.Marshal()
100 | }
101 | return oob
102 | }
103 |
--------------------------------------------------------------------------------
/third_party/dns/udp_test.go:
--------------------------------------------------------------------------------
1 | // +build linux,!appengine
2 |
3 | package dns
4 |
5 | import (
6 | "bytes"
7 | "net"
8 | "runtime"
9 | "strings"
10 | "testing"
11 | "time"
12 |
13 | "golang.org/x/net/ipv4"
14 | "golang.org/x/net/ipv6"
15 | )
16 |
17 | func TestSetUDPSocketOptions(t *testing.T) {
18 | // returns an error if we cannot resolve that address
19 | testFamily := func(n, addr string) error {
20 | a, err := net.ResolveUDPAddr(n, addr)
21 | if err != nil {
22 | return err
23 | }
24 | c, err := net.ListenUDP(n, a)
25 | if err != nil {
26 | return err
27 | }
28 | if err := setUDPSocketOptions(c); err != nil {
29 | t.Fatalf("failed to set socket options: %v", err)
30 | }
31 | ch := make(chan *SessionUDP)
32 | go func() {
33 | // Set some deadline so this goroutine doesn't hang forever
34 | c.SetReadDeadline(time.Now().Add(time.Minute))
35 | b := make([]byte, 1)
36 | _, sess, err := ReadFromSessionUDP(c, b)
37 | if err != nil {
38 | t.Errorf("failed to read from conn: %v", err)
39 | // fallthrough to chan send below
40 | }
41 | ch <- sess
42 | }()
43 |
44 | c2, err := net.Dial("udp", c.LocalAddr().String())
45 | if err != nil {
46 | t.Fatalf("failed to dial udp: %v", err)
47 | }
48 | if _, err := c2.Write([]byte{1}); err != nil {
49 | t.Fatalf("failed to write to conn: %v", err)
50 | }
51 | sess := <-ch
52 | if sess == nil {
53 | // t.Error was already called in the goroutine above.
54 | t.FailNow()
55 | }
56 | if len(sess.context) == 0 {
57 | t.Fatalf("empty session context: %v", sess)
58 | }
59 | ip := parseDstFromOOB(sess.context)
60 | if ip == nil {
61 | t.Fatalf("failed to parse dst: %v", sess)
62 | }
63 | if !strings.Contains(c.LocalAddr().String(), ip.String()) {
64 | t.Fatalf("dst was different than listen addr: %v != %v", ip.String(), c.LocalAddr().String())
65 | }
66 | return nil
67 | }
68 |
69 | // we require that ipv4 be supported
70 | if err := testFamily("udp4", "127.0.0.1:0"); err != nil {
71 | t.Fatalf("failed to test socket options on IPv4: %v", err)
72 | }
73 | // IPv6 might not be supported so these will just log
74 | if err := testFamily("udp6", "[::1]:0"); err != nil {
75 | t.Logf("failed to test socket options on IPv6-only: %v", err)
76 | }
77 | if err := testFamily("udp", "[::1]:0"); err != nil {
78 | t.Logf("failed to test socket options on IPv6/IPv4: %v", err)
79 | }
80 | }
81 |
82 | func TestParseDstFromOOB(t *testing.T) {
83 | if runtime.GOARCH != "amd64" {
84 | // The cmsghdr struct differs in the width (32/64-bit) of
85 | // lengths and the struct padding between architectures.
86 | // The data below was only written with amd64 in mind, and
87 | // thus the test must be skipped on other architectures.
88 | t.Skip("skipping test on unsupported architecture")
89 | }
90 |
91 | // dst is :ffff:100.100.100.100
92 | oob := []byte{36, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 100, 100, 100, 100, 2, 0, 0, 0}
93 | dst := parseDstFromOOB(oob)
94 | dst4 := dst.To4()
95 | if dst4 == nil {
96 | t.Errorf("failed to parse IPv4 in IPv6: %v", dst)
97 | } else if dst4.String() != "100.100.100.100" {
98 | t.Errorf("unexpected IPv4: %v", dst4)
99 | }
100 |
101 | // dst is 2001:db8::1
102 | oob = []byte{36, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 50, 0, 0, 0, 32, 1, 13, 184, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0}
103 | dst = parseDstFromOOB(oob)
104 | dst6 := dst.To16()
105 | if dst6 == nil {
106 | t.Errorf("failed to parse IPv6: %v", dst)
107 | } else if dst6.String() != "2001:db8::1" {
108 | t.Errorf("unexpected IPv6: %v", dst4)
109 | }
110 |
111 | // dst is 100.100.100.100 but was received on 10.10.10.10
112 | oob = []byte{28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 2, 0, 0, 0, 10, 10, 10, 10, 100, 100, 100, 100, 0, 0, 0, 0}
113 | dst = parseDstFromOOB(oob)
114 | dst4 = dst.To4()
115 | if dst4 == nil {
116 | t.Errorf("failed to parse IPv4: %v", dst)
117 | } else if dst4.String() != "100.100.100.100" {
118 | t.Errorf("unexpected IPv4: %v", dst4)
119 | }
120 | }
121 |
122 | func TestCorrectSource(t *testing.T) {
123 | if runtime.GOARCH != "amd64" {
124 | // See comment above in TestParseDstFromOOB.
125 | t.Skip("skipping test on unsupported architecture")
126 | }
127 |
128 | // dst is :ffff:100.100.100.100 which should be counted as IPv4
129 | oob := []byte{36, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 100, 100, 100, 100, 2, 0, 0, 0}
130 | soob := correctSource(oob)
131 | cm4 := new(ipv4.ControlMessage)
132 | cm4.Src = net.ParseIP("100.100.100.100")
133 | if !bytes.Equal(soob, cm4.Marshal()) {
134 | t.Errorf("unexpected oob for ipv4 address: %v", soob)
135 | }
136 |
137 | // dst is 2001:db8::1
138 | oob = []byte{36, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 50, 0, 0, 0, 32, 1, 13, 184, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0}
139 | soob = correctSource(oob)
140 | cm6 := new(ipv6.ControlMessage)
141 | cm6.Src = net.ParseIP("2001:db8::1")
142 | if !bytes.Equal(soob, cm6.Marshal()) {
143 | t.Errorf("unexpected oob for IPv6 address: %v", soob)
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/third_party/dns/update_test.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestDynamicUpdateUnpack(t *testing.T) {
8 | // From https://github.com/miekg/dns/issues/150#issuecomment-62296803
9 | // It should be an update message for the zone "example.",
10 | // deleting the A RRset "example." and then adding an A record at "example.".
11 | // class ANY, TYPE A
12 | buf := []byte{171, 68, 40, 0, 0, 1, 0, 0, 0, 2, 0, 0, 7, 101, 120, 97, 109, 112, 108, 101, 0, 0, 6, 0, 1, 192, 12, 0, 1, 0, 255, 0, 0, 0, 0, 0, 0, 192, 12, 0, 1, 0, 1, 0, 0, 0, 0, 0, 4, 127, 0, 0, 1}
13 | msg := new(Msg)
14 | err := msg.Unpack(buf)
15 | if err != nil {
16 | t.Errorf("failed to unpack: %v\n%s", err, msg.String())
17 | }
18 | }
19 |
20 | func TestDynamicUpdateZeroRdataUnpack(t *testing.T) {
21 | m := new(Msg)
22 | rr := &RR_Header{Name: ".", Rrtype: 0, Class: 1, Ttl: ^uint32(0), Rdlength: 0}
23 | m.Answer = []RR{rr, rr, rr, rr, rr}
24 | m.Ns = m.Answer
25 | for n, s := range TypeToString {
26 | rr.Rrtype = n
27 | bytes, err := m.Pack()
28 | if err != nil {
29 | t.Errorf("failed to pack %s: %v", s, err)
30 | continue
31 | }
32 | if err := new(Msg).Unpack(bytes); err != nil {
33 | t.Errorf("failed to unpack %s: %v", s, err)
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/third_party/imf/testdata/nested-mime:
--------------------------------------------------------------------------------
1 | --e89a8ff1c1e83553e304be640612
2 | Content-Type: multipart/alternative; boundary=e89a8ff1c1e83553e004be640610
3 |
4 | --e89a8ff1c1e83553e004be640610
5 | Content-Type: text/plain; charset=UTF-8
6 |
7 | *body*
8 |
9 | --e89a8ff1c1e83553e004be640610
10 | Content-Type: text/html; charset=UTF-8
11 |
12 | body
13 |
14 | --e89a8ff1c1e83553e004be640610--
15 | --e89a8ff1c1e83553e304be640612
16 | Content-Type: image/png; name="x.png"
17 | Content-Disposition: attachment;
18 | filename="x.png"
19 | Content-Transfer-Encoding: base64
20 | X-Attachment-Id: f_h1edgigu0
21 |
22 | iVBORw0KGgoAAAANSUhEUgAAAagAAADrCAIAAACza5XhAAAKMWlDQ1BJQ0MgUHJvZmlsZQAASImd
23 | lndUU9kWh8+9N71QkhCKlNBraFICSA29SJEuKjEJEErAkAAiNkRUcERRkaYIMijggKNDkbEiioUB
24 | 8b2kqeGaj4aTNftesu5mob4pr07ecMywRwLBvDCJOksqlUyldAZD7g9fxIZRWWPMvXRNJROJRBIG
25 | Y7Vx0mva1HAwYqibdKONXye3dW4iUonhWFJnqK7OaanU1gGkErFYEgaj0cg8wK+zVPh2ziwnHy07
26 | U8lYTNapezSzOuevRwLB7CFkqQQCwaJDiBQIBIJFhwh8AoFg0SHUqQUCASRJKkwkhMy/JfODWPEJ
27 | BIJFhwh8AoFg0TFnQqQ55GtPFopcJsN97e1nYtNuIBYeGBgYCmYrmE3jZ05iaGAoMX0xzxkWz6Hv
28 | yO7WvrlwzA0uLzrD+VkKqViwl9IfTBVNFMyc/x9alloiPPlqhQAAAABJRU5ErkJggg==
29 | --e89a8ff1c1e83553e304be640612--
30 |
--------------------------------------------------------------------------------
/third_party/imf/textproto.go:
--------------------------------------------------------------------------------
1 | // Copyright 2010 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | // Package imf is adapted from the Go standard library.
6 | package imf
7 |
8 | // Originally from go/src/net/textproto/textproto.go.
9 |
10 | import (
11 | "fmt"
12 | )
13 |
14 | // An Error represents a numeric error response from a server.
15 | type Error struct {
16 | Code int
17 | Msg string
18 | }
19 |
20 | func (e *Error) Error() string {
21 | return fmt.Sprintf("%03d %s", e.Code, e.Msg)
22 | }
23 |
24 | // A ProtocolError describes a protocol violation such
25 | // as an invalid response or a hung-up connection.
26 | type ProtocolError string
27 |
28 | func (p ProtocolError) Error() string {
29 | return string(p)
30 | }
31 |
32 | func isASCIISpace(b byte) bool {
33 | return b == ' ' || b == '\t' || b == '\n' || b == '\r'
34 | }
35 |
36 | func isASCIILetter(b byte) bool {
37 | b |= 0x20 // make lower case
38 | return 'a' <= b && b <= 'z'
39 | }
40 |
41 | // trim returns s with leading and trailing spaces and tabs removed.
42 | // It does not assume Unicode or UTF-8.
43 | func trim(s []byte) []byte {
44 | i := 0
45 | for i < len(s) && (s[i] == ' ' || s[i] == '\t') {
46 | i++
47 | }
48 | n := len(s)
49 | for n > i && (s[n-1] == ' ' || s[n-1] == '\t') {
50 | n--
51 | }
52 | return s[i:n]
53 | }
54 |
--------------------------------------------------------------------------------
/util/devcert/devcert.go:
--------------------------------------------------------------------------------
1 | // Package devcert generates a local TLS server certificate using the mkcert root CA.
2 | package devcert
3 |
4 | import (
5 | "crypto"
6 | "crypto/ecdsa"
7 | "crypto/elliptic"
8 | "crypto/rand"
9 | "crypto/tls"
10 | "crypto/x509"
11 | "crypto/x509/pkix"
12 | "encoding/pem"
13 | "fmt"
14 | "io/ioutil"
15 | "math/big"
16 | "net"
17 | "os"
18 | "path/filepath"
19 | "runtime"
20 | "sync"
21 | "time"
22 | )
23 |
24 | // Config creates a *tls.Config with a local CA serving certificate.
25 | func Config() (*tls.Config, error) {
26 | certOnce.Do(createCert)
27 |
28 | if certErr != nil {
29 | return nil, certErr
30 | }
31 |
32 | return &tls.Config{
33 | Certificates: []tls.Certificate{cert},
34 | }, nil
35 | }
36 |
37 | var (
38 | cert tls.Certificate
39 | certErr error
40 | certOnce sync.Once
41 | )
42 |
43 | func createCert() {
44 | caCert, caKey, err := loadCA()
45 | if err != nil {
46 | if os.IsNotExist(err) {
47 | certErr = fmt.Errorf("devcert: no mkcert CA root found.\n\n" +
48 | "Install a development CA root by running:\n" +
49 | "\tgo get -u github.com/FiloSottile/mkcert\n" +
50 | "\tmkcert -install\n")
51 | return
52 | }
53 | certErr = fmt.Errorf("devcert: mkcert CA load failed: %v", err)
54 | return
55 | }
56 |
57 | priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
58 | //priv, err := rsa.GenerateKey(rand.Reader, 2048)
59 | if err != nil {
60 | certErr = fmt.Errorf("devcert: %v", err)
61 | return
62 | }
63 |
64 | serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 64)
65 | serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
66 | if err != nil {
67 | certErr = fmt.Errorf("devcert: cannot generate a serial number: %v", err)
68 | return
69 | }
70 |
71 | tmpl := &x509.Certificate{
72 | SerialNumber: serialNumber,
73 | Subject: pkix.Name{
74 | Organization: []string{"local spilld development certificate"},
75 | OrganizationalUnit: []string{"local spilld developer"},
76 | },
77 | NotAfter: time.Now().AddDate(1, 1, 0),
78 | NotBefore: time.Now(),
79 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
80 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
81 |
82 | BasicConstraintsValid: true,
83 | IPAddresses: []net.IP{
84 | net.ParseIP("127.0.0.1"),
85 | net.ParseIP("::1"),
86 | },
87 | DNSNames: []string{"localhost"},
88 | }
89 | // TODO: include net.Interfaces() in IPAddresses
90 | if host, _ := os.Hostname(); host != "" {
91 | tmpl.DNSNames = append(tmpl.DNSNames, host)
92 | }
93 |
94 | certBytes, err := x509.CreateCertificate(rand.Reader, tmpl, caCert, priv.Public(), caKey)
95 | if err != nil {
96 | certErr = fmt.Errorf("devcert: %v", err)
97 | return
98 | }
99 |
100 | cert.Certificate = append(cert.Certificate, certBytes)
101 | cert.PrivateKey = priv
102 | }
103 |
104 | func readFilePEM(path string) ([]byte, error) {
105 | pemb, err := ioutil.ReadFile(path)
106 | if err != nil {
107 | return nil, err
108 | }
109 | der, _ := pem.Decode(pemb)
110 | if der == nil {
111 | return nil, fmt.Errorf("could not PEM decode %s", path)
112 | }
113 | return der.Bytes, nil
114 | }
115 |
116 | func loadCA() (*x509.Certificate, crypto.PrivateKey, error) {
117 | dir := filepath.Join(osCertDir(), "mkcert")
118 |
119 | certBytes, err := readFilePEM(filepath.Join(dir, "rootCA.pem"))
120 | if err != nil {
121 | return nil, nil, err
122 | }
123 | caCert, err := x509.ParseCertificate(certBytes)
124 | if err != nil {
125 | return nil, nil, err
126 | }
127 |
128 | keyBytes, err := readFilePEM(filepath.Join(dir, "rootCA-key.pem"))
129 | if err != nil {
130 | return nil, nil, err
131 | }
132 | caKey, err := x509.ParsePKCS8PrivateKey(keyBytes)
133 | if err != nil {
134 | return nil, nil, err
135 | }
136 | return caCert, caKey, nil
137 | }
138 |
139 | // osCertDir returns the directory that mkcert uses for storing its root CA key.
140 | // This function needs to produce the same result as mkcert.
141 | func osCertDir() string {
142 | switch {
143 | case runtime.GOOS == "windows":
144 | return os.Getenv("LocalAppData")
145 | case os.Getenv("XDG_DATA_HOME") != "":
146 | return os.Getenv("XDG_DATA_HOME")
147 | default:
148 | dir := os.Getenv("HOME")
149 | if dir == "" {
150 | return ""
151 | }
152 | if runtime.GOOS == "darwin" {
153 | return filepath.Join(dir, "Library", "Application Support")
154 | }
155 | return filepath.Join(dir, ".local", "share")
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/util/throttle/throttle.go:
--------------------------------------------------------------------------------
1 | package throttle
2 |
3 | import (
4 | "sync"
5 | "time"
6 | )
7 |
8 | type Throttle struct {
9 | mu sync.Mutex
10 | attempts map[string]state
11 | cleaned time.Time
12 | }
13 |
14 | type state struct {
15 | last time.Time
16 | failures int
17 | }
18 |
19 | func (tr *Throttle) Throttle(val string) bool {
20 | const delay = 3 * time.Second
21 | const window = 60 * time.Second
22 | const buffer = 10
23 |
24 | now := timeNow()
25 |
26 | tr.mu.Lock()
27 | if now.Sub(tr.cleaned) > window {
28 | // Cleanup old keys.
29 | for key, tm := range tr.attempts {
30 | if now.Sub(tm.last) > delay {
31 | delete(tr.attempts, key)
32 | }
33 | }
34 | tr.cleaned = now
35 | }
36 | state := tr.attempts[val]
37 | tr.mu.Unlock()
38 |
39 | if state.failures >= buffer && now.Sub(state.last) < delay {
40 | timeSleep(delay)
41 | return true
42 | }
43 | return false
44 | }
45 |
46 | func (tr *Throttle) Add(val string) {
47 | tr.mu.Lock()
48 | if tr.attempts == nil {
49 | tr.attempts = make(map[string]state)
50 | }
51 | state := tr.attempts[val]
52 | state.last = timeNow()
53 | state.failures++
54 | tr.attempts[val] = state
55 | tr.mu.Unlock()
56 | }
57 |
58 | var timeSleep = time.Sleep
59 | var timeNow = time.Now
60 |
--------------------------------------------------------------------------------
/util/throttle/throttle_test.go:
--------------------------------------------------------------------------------
1 | package throttle
2 |
3 | import (
4 | "testing"
5 | "time"
6 | )
7 |
8 | func TestThrottle(t *testing.T) {
9 | now := time.Now()
10 | slept := time.Duration(0)
11 | timeSleep = func(d time.Duration) { slept = d }
12 | timeNow = func() time.Time { return now }
13 | defer func() {
14 | timeSleep = time.Sleep
15 | timeNow = time.Now
16 | }()
17 |
18 | tr := Throttle{}
19 | if tr.Throttle("foo") || slept != 0 {
20 | // interal map not yet initialized
21 | t.Errorf("empty throttle is throttling: %v", slept)
22 | slept = 0
23 | }
24 |
25 | tr.Add("foo")
26 | if tr.Throttle("foo") || slept != 0 {
27 | t.Errorf("throttling inside initial buffer: %v", slept)
28 | slept = 0
29 | }
30 | for i := 0; i < 10; i++ {
31 | tr.Add("foo")
32 | }
33 | if !tr.Throttle("foo") || slept != 3*time.Second {
34 | t.Errorf("want throttling, got: %v", slept)
35 | }
36 | slept = 0
37 | now = now.Add(4 * time.Second)
38 | if tr.Throttle("foo") || slept != 0 {
39 | t.Errorf("throttling after sufficient wait: %v", slept)
40 | }
41 | slept = 0
42 |
43 | now = now.Add(61 * time.Second)
44 |
45 | if tr.Throttle("foo") || slept != 0 {
46 | t.Errorf("throttling after cleaning window: %v", slept)
47 | slept = 0
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/util/tlstest/tlstest.go:
--------------------------------------------------------------------------------
1 | // Package tlstest provides a pair of TLS configurations for testing.
2 | //
3 | // It can be used to establish TLS connections in tests:
4 | //
5 | // srv := &http.Server{
6 | // TLSConfig: tlstest.ServerConfig,
7 | // }
8 | // // ...call srv.ServeTLS
9 | //
10 | // client := &http.Client{
11 | // Transport: &http.Transport{TLSClientConfig: tlstest.ClientConfig},
12 | // }
13 | // resp, err := client.Get("https://localaddr-of-srv/")
14 | // // ...
15 | //
16 | // Note that if you are exclusively testing with HTTP, the standard
17 | // library provides this in net/http/httptest.
18 | //
19 | // This package is intended for more general-purpose TLS socket testing.
20 | package tlstest
21 |
22 | import (
23 | "crypto/tls"
24 | "crypto/x509"
25 | "fmt"
26 |
27 | _ "testing"
28 | )
29 |
30 | var ClientConfig = initClientConfig()
31 | var ServerConfig = &tls.Config{
32 | Certificates: []tls.Certificate{cert},
33 | }
34 |
35 | func initClientConfig() *tls.Config {
36 | certpool := x509.NewCertPool()
37 | certificate, err := x509.ParseCertificate(cert.Certificate[0])
38 | if err != nil {
39 | panic(fmt.Sprintf("tlstest: %v", err))
40 | }
41 | certpool.AddCert(certificate)
42 | return &tls.Config{
43 | RootCAs: certpool,
44 | }
45 | }
46 |
47 | var cert = initCert()
48 |
49 | func initCert() tls.Certificate {
50 | cert, err := tls.X509KeyPair([]byte(testCert), []byte(testKey))
51 | if err != nil {
52 | panic(fmt.Sprintf("tlstest: %v", err))
53 | }
54 | return cert
55 | }
56 |
57 | // Generated using GOROOT/src/crypto/tls:
58 | // go run generate_cert.go -rsa-bits 2048 --host 127.0.0.1,::1,::,example.com,localhost \
59 | // --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
60 | const testCert = `-----BEGIN CERTIFICATE-----
61 | MIIDNzCCAh+gAwIBAgIQL4Vm/jbjVDL1W/+QmE6GizANBgkqhkiG9w0BAQsFADAS
62 | MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw
63 | MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
64 | MIIBCgKCAQEA60v8OepbfpjeHtmw48R8yPdW4XXyWQGWCfwIQ0UqdVZX9cT9L1Dx
65 | 2Em3Pu11LWhfIDApgqVFHy7PdIY+fhKNPMui7Qh/y7OSIO71wWcL0G5yoW8exiGa
66 | /w61sZFa56KPxhC09k0pX86a6VOufxKs79foVlTPM+iCBRvsryYodUJjdsY9WZlO
67 | VBZvDEVOkcf58CwgkBYO8WbaVxK6tuvL66pOrUaKSZUzFAE9zpIwavKucNaYTod7
68 | HCSjBHhJ+YqRvudFNQWyLF2jQYHFaUN4DpjFJwy/8vZ8XdaOoxQKMCJWbtO7+GG8
69 | 0mrkXUKxfnAMZDGo0WoGWt7xPsYwPmwQ0QIDAQABo4GGMIGDMA4GA1UdDwEB/wQE
70 | AwICpDATBgNVHSUEDDAKBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MEsGA1Ud
71 | EQREMEKCC2V4YW1wbGUuY29tgglsb2NhbGhvc3SHBH8AAAGHEAAAAAAAAAAAAAAA
72 | AAAAAAGHEAAAAAAAAAAAAAAAAAAAAAAwDQYJKoZIhvcNAQELBQADggEBAMZiLweQ
73 | t4BQ0t56paXh/o5FEdHdEtK+JT9JtBSI6ZHLrNj0riGshPdLJYgLbU4g8mhzZ/Ob
74 | snSXCf2sJqSVLNfMneFoLEXp9e5xeOGQMcbuV84NlbYb7reFZk/Eex1pnCUtlPHH
75 | AB+c1Y/QQFlj1qbUI4P03O7pAGh979WdYOOp9/XpO52VI9pMCaYOEnkEUNjvm4Ja
76 | BjCZBDrQYCBZHRQLQ7+EjvRfWLPqBjf9Z6U8R3ey9+1CX5k4zo3Z7hhoEyPnncZ9
77 | duelzBygFffq9za6iKW+aIkkNtDLr64H5yLtoDZdc2MMXRzMEv2qtyM4/VLmtQ3r
78 | DW5w0S9gkD6oxaM=
79 | -----END CERTIFICATE-----`
80 |
81 | const testKey = `-----BEGIN RSA PRIVATE KEY-----
82 | MIIEpQIBAAKCAQEA60v8OepbfpjeHtmw48R8yPdW4XXyWQGWCfwIQ0UqdVZX9cT9
83 | L1Dx2Em3Pu11LWhfIDApgqVFHy7PdIY+fhKNPMui7Qh/y7OSIO71wWcL0G5yoW8e
84 | xiGa/w61sZFa56KPxhC09k0pX86a6VOufxKs79foVlTPM+iCBRvsryYodUJjdsY9
85 | WZlOVBZvDEVOkcf58CwgkBYO8WbaVxK6tuvL66pOrUaKSZUzFAE9zpIwavKucNaY
86 | Tod7HCSjBHhJ+YqRvudFNQWyLF2jQYHFaUN4DpjFJwy/8vZ8XdaOoxQKMCJWbtO7
87 | +GG80mrkXUKxfnAMZDGo0WoGWt7xPsYwPmwQ0QIDAQABAoIBAHNgpyWfDY5eV0y5
88 | YkvNpYLGBgw4UcXjSTdMJqEV4WP4Gtmg5qW1A2ITg4+P0M2bSEn4U+KEOAi6Y2+4
89 | BBy97BPLpvCkIkY4n4cWpdtYNCrYfc07N9Pf1qkLBX000WaUB/wPZS0BWTBplvyi
90 | 1AXrmnFhZcQvggrqEBeBQeYAyAX2vxhPPy0pHoUmGTJERm6J8zpK1HqKQpzE9foX
91 | xEGgVCH3Zgo3ZsBlIHCVF/VuTnoMhhwlS2JBdr57npv2fw+HsfY/ophYerJokH7r
92 | hUUhzNO4wPkdOZkKgIx53jAWLDl7ZSN8rUo/X0ix/UEMgr+1iM5hOlXFEgvQuH1J
93 | +xmRESECgYEA+9Y1DXRSu7KOLLbAlslvLgLwIYRqHukjlFv4s0927oGgOrfLPNEi
94 | PSp92pphqEYFqqrkDzuerKIRE/d6BvDGbOvrK/7BEL+GArnrSN2T7A7rGubw2AIb
95 | t41Y3RETz6HxAk9GiEbBb/hCD4qGDW0wqYDphToh3Kys7Cd2N/aGdxcCgYEA7y/H
96 | napziUbPE0yNgcgWwlViHbhjPs8qNLfCCDi03efgVdeMoRWzo5ekE4W2hekwDnlx
97 | /1vXNZdPKzDEsLpTVNQegWjH42zxmv1Dek9XdSPEspWFOowxjiSYHrkIOWk7mZjQ
98 | TXLxepvzH76Vm7+8WuP3c8Ur7qkC48bNg1+SKFcCgYEAkennC0ietwoZvmaU58kG
99 | lg41u/XQ1uAWMVuomZwtOLv6bosXQsGZqP75tLNGag1IMz6YrQrKQRQV+Q+msGbJ
100 | UUrQE8mja2TM7L90R9+6WUe7iPbODRoLnSpUlqHSbLdTwRbVsxfr9EhPXlnQme7u
101 | BwgeRYcNH6Mc/idPI9W+yzkCgYEAgQ0mhssQy2CJGcCUGRH8NZ4b8i0qXxknjIoZ
102 | BpaR/6i8QZSrK76pzfpjbKUYdef7JdQgzcaftyqMbKFDfpcJnxtT2j7OmsaNFTLQ
103 | 1Y05gtpppnFGEPDTS/4ylWEALvm4TodE3ITIBX9fDiGmVwJ8fg3B1ZTsvzgxdvQs
104 | rlVCZsECgYEArpTuVyIAahDFRsaTnG0mEOkINBvBUmu+9+U5E6kGVQOyR/UgAYtK
105 | YUCZ7E7fCBBBeWzJDtaL0PLn/78HJwOidJxqa0HCI3sTNXnyin+qDKg7RdvLDYwx
106 | R4W4owFagG6iKO/I3q0ZZ1sm+DV4XGzDv166CeSFdi2vvprT8F145gw=
107 | -----END RSA PRIVATE KEY-----`
108 |
--------------------------------------------------------------------------------