├── 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 bolditalic, 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: "hello

hello
world

", 21 | out: "

hello
world

", 22 | opts: StrictEmail, 23 | }, 24 | { 25 | name: "banal-safe", 26 | in: "hello

hello
world

", 27 | out: "hello

hello
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 | --------------------------------------------------------------------------------