├── .gitignore ├── AUTHORS ├── CONTRIBUTORS ├── COPYING ├── Makefile ├── README ├── addr.go ├── email.go ├── email_test.go ├── testdata ├── facebook_dkim.txt ├── gmail_dkim.txt └── twitter_dkim.txt ├── webfist.go ├── webfist_test.go └── webfistd ├── add.go ├── blob.go ├── front.go ├── localdisk.go ├── lookup.go ├── lookup_test.go ├── recent.go ├── smtp.go ├── sync.go └── webfistd.go /.gitignore: -------------------------------------------------------------------------------- 1 | webfist 2 | webfistd/webfistd 3 | *~ 4 | \#* 5 | \.\#* 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the official list of WebFist authors for copyright purposes. 2 | # This file is distinct from the CONTRIBUTORS files. 3 | # See the latter for an explanation. 4 | 5 | Google Inc. 6 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | # People who have agreed to one of the CLAs and can contribute patches. 2 | # The AUTHORS file lists the copyright holders; this file 3 | # lists people. For example, Google employees are listed here 4 | # but not in AUTHORS, because Google holds the copyright. 5 | # 6 | # http://code.google.com/legal/individual-cla-v1.0.html (electronic submission) 7 | # http://code.google.com/legal/corporate-cla-v1.0.html (requires FAX) 8 | # 9 | # Note that the CLA isn't a copyright _assigment_ but rather a 10 | # copyright _license_. You retain the copyright on your 11 | # contributions. 12 | 13 | Brad Fitzpatrick 14 | Brett Slatkin 15 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ssh: 2 | ssh -i ~/keys/webfist.pem ubuntu@webfist.org 3 | 4 | runprod: 5 | cd $(GOPATH)/src/github.com/bradfitz/webfist/webfistd 6 | go build 7 | sudo ./webfistd -web=80 -smtp=25 8 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | WebFist implements WebFinger delegation for providers who don't 2 | support WebFinger natively. 3 | 4 | It takes advantage of the fact that all major providers DKIM-sign 5 | their outgoing emails. 6 | 7 | So if you have a Gmail, Facebook, Yahoo, Outlook, or whatever account, 8 | you can email a server in the WebFist pool of servers, the server will 9 | DKIM-verify it, parse it for a WebFinger delegation command, and then 10 | encrypt your original email (with your email address as the key) and 11 | then replicate the encrypted data across the network of WebFist servers. 12 | 13 | Each WebFist node is then also a WebFinger server, so you can do 14 | WebFinger lookups on gmail or facebook email addresses. 15 | 16 | Consider it a WebFinger fallback. 17 | 18 | One node is currently running at http://webfist.org/ 19 | 20 | The plan is to have a big pool of WebFist servers, like NTP pools. 21 | 22 | Written by Brad Fitzpatrick and Brett Slatkin at IndieWebCamp in 23 | Portland on 2013-06-23. 24 | 25 | STATUS: quick hack, made while racing against demo time. It works, but 26 | could use some polish. 27 | -------------------------------------------------------------------------------- /addr.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2013 WebFist AUTHORS 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package webfist 18 | 19 | import ( 20 | "crypto/sha1" 21 | "io" 22 | "sync" 23 | 24 | "crypto/aes" 25 | "crypto/cipher" 26 | "fmt" 27 | 28 | "code.google.com/p/go.crypto/scrypt" 29 | ) 30 | 31 | var ( 32 | dummyIV = make([]byte, 16) // all zeros 33 | fistSalt = []byte("WebFist salt.") 34 | ) 35 | 36 | var keyCache struct { 37 | sync.Mutex 38 | m map[string][]byte 39 | } 40 | 41 | // EmailAddr provides utility functions on a wrapped email address. 42 | type EmailAddr struct { 43 | email string // canonical 44 | 45 | keyOnce sync.Once 46 | lazyKey []byte // scrypt 47 | } 48 | 49 | // NewEmailAddr returns a EmailAddr wrapper around an email address string. 50 | // The incoming email address does not need to be canonicalized. 51 | func NewEmailAddr(addr string) *EmailAddr { 52 | return &EmailAddr{ 53 | email: canonicalEmail(addr), 54 | } 55 | } 56 | 57 | // Canonical returns the canonical version of the email address. 58 | func (e *EmailAddr) Canonical() string { 59 | return e.email 60 | } 61 | 62 | // HexKey returns the human-readable, lowercase hex version of 63 | // the email address's key. 64 | func (e *EmailAddr) HexKey() string { 65 | return fmt.Sprintf("%x", e.getKey()[:20]) 66 | } 67 | 68 | func (e *EmailAddr) getKey() []byte { 69 | e.keyOnce.Do(e.initLazyKey) 70 | return e.lazyKey 71 | } 72 | 73 | func (e *EmailAddr) initLazyKey() { 74 | emailKey := e.Canonical() 75 | keyCache.Lock() 76 | v, ok := keyCache.m[emailKey] 77 | keyCache.Unlock() 78 | 79 | if ok { 80 | e.lazyKey = v 81 | return 82 | } 83 | 84 | key, err := scrypt.Key([]byte(emailKey), fistSalt, 16384*8, 8, 1, 32) 85 | if err != nil { 86 | panic(err) 87 | } 88 | e.lazyKey = key 89 | 90 | keyCache.Lock() 91 | if keyCache.m == nil { 92 | keyCache.m = make(map[string][]byte) 93 | } 94 | keyCache.m[emailKey] = key 95 | // TODO: Prune the cache 96 | keyCache.Unlock() 97 | } 98 | 99 | func (e *EmailAddr) block() cipher.Block { 100 | block, err := aes.NewCipher(e.encryptionKey()) 101 | if err != nil { 102 | panic(err) 103 | } 104 | return block 105 | } 106 | 107 | // encryptionKey returns the AES-128 key for this email address. 108 | func (e *EmailAddr) encryptionKey() []byte { 109 | s1 := sha1.New() 110 | io.WriteString(s1, e.email) 111 | s1.Write(e.getKey()) 112 | return s1.Sum(nil)[:16] 113 | } 114 | 115 | // canonicalEmail returns the canonicalized version of the provided 116 | // email address. 117 | func canonicalEmail(email string) string { 118 | // TODO 119 | return email 120 | } 121 | 122 | func (e *EmailAddr) Encrypter(w io.Writer) io.Writer { 123 | return cipher.StreamWriter{ 124 | S: cipher.NewCTR(e.block(), dummyIV), 125 | W: w, 126 | } 127 | } 128 | 129 | func (e *EmailAddr) Decrypter(r io.Reader) io.Reader { 130 | return cipher.StreamReader{ 131 | S: cipher.NewCTR(e.block(), dummyIV), 132 | R: r, 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /email.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2013 WebFist AUTHORS 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package webfist 18 | 19 | import ( 20 | "bytes" 21 | "crypto/sha1" 22 | "errors" 23 | "fmt" 24 | "io" 25 | "io/ioutil" 26 | "log" 27 | "net/mail" 28 | "os/exec" 29 | "regexp" 30 | "strings" 31 | "sync" 32 | "time" 33 | ) 34 | 35 | // MaxEmailSize is the maxium size of an RFC 822 email, including 36 | // both its headers and body. 37 | const MaxEmailSize = 64 << 10 38 | 39 | // Email wraps a signed email. 40 | type Email struct { 41 | all []byte 42 | msg *mail.Message 43 | body []byte 44 | 45 | encSHA1 string // Lazy 46 | } 47 | 48 | // NewEmail parses all as an email and returns a wrapper around it. 49 | // Its size and format is done, but no signing verification is done. 50 | func NewEmail(all []byte) (*Email, error) { 51 | if len(all) > MaxEmailSize { 52 | return nil, errors.New("email too large") 53 | } 54 | msg, err := mail.ReadMessage(bytes.NewReader(all)) 55 | if err != nil { 56 | return nil, err 57 | } 58 | body, err := ioutil.ReadAll(msg.Body) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | // TODO: Extract the message receive time for sorting purposes 64 | e := &Email{ 65 | all: all, 66 | msg: msg, 67 | body: body, 68 | } 69 | return e, nil 70 | } 71 | 72 | // Verify returns whether 73 | func (e *Email) Verify() bool { 74 | dkimVerifyOnce.Do(initDKIMVerify) 75 | if e.msg.Header.Get("DKIM-Signature") == "" { 76 | return false 77 | } 78 | 79 | cmd := exec.Command(dkimVerifyPath) 80 | cmd.Stdin = bytes.NewReader(e.all) 81 | out, err := cmd.CombinedOutput() 82 | if err == nil && strings.TrimSpace(string(out)) == "signature ok" { 83 | return true 84 | } 85 | return false 86 | } 87 | 88 | func (e *Email) From() (*EmailAddr, error) { 89 | mailAddr, err := mail.ParseAddress(e.msg.Header.Get("From")) 90 | if err != nil { 91 | return nil, err 92 | } 93 | return NewEmailAddr(mailAddr.Address), nil 94 | } 95 | 96 | func (e *Email) Date() (time.Time, error) { 97 | t, err := e.msg.Header.Date() 98 | if err != nil { 99 | return time.Time{}, err 100 | } 101 | return t, nil 102 | } 103 | 104 | // EncSHA1 returns a lowercase SHA1 hex of the encrypted email. 105 | func (e *Email) EncSHA1() (string, error) { 106 | if e.encSHA1 != "" { 107 | return e.encSHA1, nil 108 | } 109 | r, err := e.Encrypted() 110 | if err != nil { 111 | return "", err 112 | } 113 | s1 := sha1.New() 114 | _, err = io.Copy(s1, r) 115 | if err != nil { 116 | return "", nil 117 | } 118 | 119 | e.encSHA1 = fmt.Sprintf("%x", s1.Sum(nil)) 120 | return e.encSHA1, nil 121 | } 122 | 123 | func (e *Email) SetEncSHA1(x string) { 124 | e.encSHA1 = x 125 | } 126 | 127 | func (e *Email) Encrypted() (io.Reader, error) { 128 | addr, err := e.From() 129 | if err != nil { 130 | return nil, err 131 | } 132 | pr, pw := io.Pipe() 133 | ew := addr.Encrypter(pw) 134 | go func() { 135 | _, err := ew.Write(e.all) 136 | pw.CloseWithError(err) 137 | }() 138 | return pr, nil 139 | } 140 | 141 | //webfist=http://example.com/myjrd.json 142 | var ( 143 | assignmentRe = regexp.MustCompile(`\bwebfist\s*=\s*(\S+)`) 144 | ) 145 | 146 | // WebFist returns the delegation identifier parse from the email. The email 147 | // must contain a single assignment where the delegated WebFinger server lives. 148 | // webfist = http://example.com/my-profile.json 149 | func (e *Email) WebFist() (string, error) { 150 | for _, match := range assignmentRe.FindAllSubmatch(e.body, -1) { 151 | if len(match) != 2 { 152 | continue 153 | } 154 | return string(match[1]), nil 155 | } 156 | return "", errors.New("'webfist' assignment missing") 157 | } 158 | 159 | var ( 160 | dkimVerifyOnce sync.Once 161 | dkimVerifyPath string 162 | ) 163 | 164 | const dkimFailMessage = "dkimverify / dkimverify.py not found. Install python-dkim (http://hewgill.com/pydkim/)" 165 | 166 | func initDKIMVerify() { 167 | path, err := findDKIMVerify() 168 | if err != nil { 169 | log.Fatalf(dkimFailMessage) 170 | } 171 | dkimVerifyPath = path 172 | } 173 | 174 | func findDKIMVerify() (path string, err error) { 175 | for _, name := range []string{"dkimverify.py", "dkimverify"} { 176 | path, err = exec.LookPath(name) 177 | if err == nil { 178 | break 179 | } 180 | } 181 | return 182 | } 183 | -------------------------------------------------------------------------------- /email_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2013 WebFist AUTHORS 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package webfist 18 | 19 | import ( 20 | "io/ioutil" 21 | "path/filepath" 22 | "testing" 23 | ) 24 | 25 | func TestDKIMVerifyAvailable(t *testing.T) { 26 | path, err := findDKIMVerify() 27 | if err != nil { 28 | t.Errorf(dkimFailMessage) 29 | } 30 | t.Logf("dkimverify at %s", path) 31 | } 32 | 33 | func TestEmailVerify(t *testing.T) { 34 | files := []string{ 35 | "gmail_dkim.txt", 36 | "facebook_dkim.txt", 37 | "twitter_dkim.txt", 38 | } 39 | for _, file := range files { 40 | full := filepath.Join("testdata", file) 41 | all, err := ioutil.ReadFile(full) 42 | if err != nil { 43 | t.Fatalf("Error opening %v: %v", full, err) 44 | } 45 | e, err := NewEmail(all) 46 | if err != nil { 47 | t.Errorf("NewEmail(%s) = %v", file, err) 48 | continue 49 | } 50 | if !e.Verify() { 51 | t.Errorf("%s didn't verify", file) 52 | } 53 | ea, err := e.From() 54 | if err != nil { 55 | t.Errorf("%s From error = %v", file, err) 56 | } else { 57 | t.Logf("%s From = %v", file, ea.Canonical()) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /testdata/facebook_dkim.txt: -------------------------------------------------------------------------------- 1 | Delivered-To: bradfitz@gmail.com 2 | Received: by 10.60.7.105 with SMTP id i9csp48710oea; 3 | Fri, 21 Jun 2013 23:24:49 -0700 (PDT) 4 | X-Received: by 10.229.56.197 with SMTP id z5mr2051560qcg.22.1371882288874; 5 | Fri, 21 Jun 2013 23:24:48 -0700 (PDT) 6 | Return-Path: 7 | Received: from mail-qa0-x230.google.com (mail-qa0-x230.google.com [2607:f8b0:400d:c00::230]) 8 | by mx.google.com with ESMTPS id z10si3907351qal.115.2013.06.21.23.24.48 9 | for 10 | (version=TLSv1 cipher=ECDHE-RSA-RC4-SHA bits=128/128); 11 | Fri, 21 Jun 2013 23:24:48 -0700 (PDT) 12 | Received-SPF: fail (google.com: domain of bradfitz@facebook.com does not designate 2607:f8b0:400d:c00::230 as permitted sender) client-ip=2607:f8b0:400d:c00::230; 13 | Authentication-Results: mx.google.com; 14 | spf=hardfail (google.com: domain of bradfitz@facebook.com does not designate 2607:f8b0:400d:c00::230 as permitted sender) smtp.mail=bradfitz@facebook.com; 15 | dkim=pass header.i=@facebook.com; 16 | dmarc=pass (p=REJECT dis=NONE) d=facebook.com 17 | Received: by mail-qa0-x230.google.com with SMTP id cm16so1143700qab.0 18 | for ; Fri, 21 Jun 2013 23:24:48 -0700 (PDT) 19 | X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; 20 | d=google.com; s=20120113; 21 | h=dkim-signature:date:from:to:message-id:in-reply-to:references 22 | :subject:mime-version:content-type:delivered-to:x-gm-message-state; 23 | bh=pdK6qXgdo9P/niPPLEsaYJJ0UZMt+JggYJ6sMOJ0quI=; 24 | b=TwJ+BscsTyJ0Z9jf7K2yE143LoHO2czcmGiSd6+jiwIMKyWb8YbgSaRwPgFhUNGTB+ 25 | BjPkUD49/kRVhSsdP96//lIjplCvSkS2zFJwzYZ1NoYkwkP4z2qNUWScuv62JwO8HdjL 26 | Yw3Dh9b8MVQCniWBeEiB7ZMkIudtWiMObFagNBMa0X/e62C87180Lm+YNlaQDJh7HKJM 27 | HRw58CZq6dzJpAhhPmBDpSsjCbNT5egM8LQptsizhTmzbsC49ZbDsV27iKDDGvd/bKaw 28 | kEZYv8eDkO1n5NVOKJyAV6nFgos4/DJW7KLwOCP0mUbmnTg86iqveIE1zDqGSAWhL3Pb 29 | vaxA== 30 | X-Received: by 10.49.62.33 with SMTP id v1mr17825022qer.53.1371882288705; 31 | Fri, 21 Jun 2013 23:24:48 -0700 (PDT) 32 | X-Received: by 10.49.62.33 with SMTP id v1mr17825005qer.53.1371882288419; 33 | Fri, 21 Jun 2013 23:24:48 -0700 (PDT) 34 | Return-Path: 35 | Received: from smtpout.mx.facebook.com (smtpout002.ash3.facebook.com. [69.171.244.65]) 36 | by mx.google.com with ESMTP id v1si4086958qef.127.2013.06.21.23.24.48 37 | for ; 38 | Fri, 21 Jun 2013 23:24:48 -0700 (PDT) 39 | Received-SPF: pass (google.com: domain of bradfitz@facebook.com designates 69.171.244.65 as permitted sender) client-ip=69.171.244.65; 40 | Return-Path: 41 | DKIM-Signature: v=1; a=rsa-sha256; d=facebook.com; s=s1024-2010-q3; c=relaxed/simple; 42 | q=dns/txt; i=@facebook.com; t=1371882288; 43 | h=From:Subject:X-:Date:To:MIME-Version:Content-Type; 44 | bh=pdK6qXgdo9P/niPPLEsaYJJ0UZMt+JggYJ6sMOJ0quI=; 45 | b=F851/bseXai+X0BMC5SmU5NMn4kK9ve4mhvJwNV6VSJNamQ79p05XfxS896kNocN 46 | IZNqqZLC05l1K4/xJs6T9uOZz3AJFvGD2yz59bAOQ4bIMLrktDTFK0isGrlwsQ8o 47 | LwrQKdYYVj2OgKdZspDIHnBXFcfqRhYmPVoM/c/SpGw=; 48 | Received: from [10.148.159.77] ([10.148.159.77:47870] helo=www.facebook.com) 49 | by 10.148.128.25 (envelope-from ) 50 | (ecelerity 2.2.3.50 r(45166/45167)) with ESMTP 51 | id 81/33-20247-03345C15; Fri, 21 Jun 2013 23:24:48 -0700 52 | Date: Fri, 21 Jun 2013 23:24:48 -0700 53 | From: Brad Fitzpatrick 54 | To: Brad Fitzpatrick 55 | Message-ID: <51c54330.a118310a.1303.ffff960fSMTPIN_ADDED_BROKEN@mx.google.com> 56 | In-Reply-To: 57 | References: 58 | Subject: Conversation with Brad Fitzpatrick 59 | MIME-Version: 1.0 60 | Content-Type: multipart/alternative; 61 | boundary="----=_Part_9281_171039419.1371882288111" 62 | Delivered-To: brad@danga.com 63 | X-Gm-Message-State: ALoCoQmWfUbsFcIQEz6qyk9oXakSYooJdEMV5v+e2UJG7uwYykVeqcc+ezNtCvHQfWV0+9SdWqCMZX8JJj2caoNNhnXphv5yaD1ZF+ar1lctA1I8C/gDv9g= 64 | 65 | ------=_Part_9281_171039419.1371882288111 66 | Content-Type: text/plain; charset=UTF-8 67 | Content-Transfer-Encoding: 7bit 68 | 69 | testing DKIM from facebook 70 | 71 | 72 | ------=_Part_9281_171039419.1371882288111 73 | Content-Type: text/html; charset=UTF-8 74 | Content-Transfer-Encoding: 7bit 75 | 76 |
testing DKIM from facebook

77 | ------=_Part_9281_171039419.1371882288111-- 78 | -------------------------------------------------------------------------------- /testdata/gmail_dkim.txt: -------------------------------------------------------------------------------- 1 | Delivered-To: bradfitz@gmail.com 2 | Received: by 10.60.7.105 with SMTP id i9csp120407oea; 3 | Sun, 23 Jun 2013 12:54:08 -0700 (PDT) 4 | X-Received: by 10.180.11.146 with SMTP id q18mr4162877wib.50.1372017247550; 5 | Sun, 23 Jun 2013 12:54:07 -0700 (PDT) 6 | Return-Path: 7 | Received: from mail-ve0-x232.google.com (mail-ve0-x232.google.com [2607:f8b0:400c:c01::232]) 8 | by mx.google.com with ESMTPS id wm3si5091471wjc.53.2013.06.23.12.54.06 9 | for 10 | (version=TLSv1 cipher=ECDHE-RSA-RC4-SHA bits=128/128); 11 | Sun, 23 Jun 2013 12:54:07 -0700 (PDT) 12 | Received-SPF: pass (google.com: domain of bslatkin@gmail.com designates 2607:f8b0:400c:c01::232 as permitted sender) client-ip=2607:f8b0:400c:c01::232; 13 | Authentication-Results: mx.google.com; 14 | spf=pass (google.com: domain of bslatkin@gmail.com designates 2607:f8b0:400c:c01::232 as permitted sender) smtp.mail=bslatkin@gmail.com; 15 | dkim=pass header.i=@gmail.com 16 | Received: by mail-ve0-x232.google.com with SMTP id pb11so8140794veb.9 17 | for ; Sun, 23 Jun 2013 12:54:06 -0700 (PDT) 18 | X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; 19 | d=google.com; s=20120113; 20 | h=dkim-signature:mime-version:date:message-id:subject:from:to 21 | :content-type:delivered-to:x-gm-message-state; 22 | bh=CZnsNv0b4VreQhSqIaKaPPbnHlllv0UQlqvG8YKbXgU=; 23 | b=Fi9EPk/AC3Ojkq/aWMqPvu+WZ57QJv4dvef/WFrVubYjaJjckcv35MOvVao+k3uDLf 24 | A5PbfAvKCHUIlTe13NfHirQBbfdylsqGIMPy8/CdcSPJU3+5yaHAXx3b0ZVWFH4dsfmx 25 | xU0rdtPRncVAfnKVG4HlelanQQZZ6AHMPDD8gHe9vqmoaLnYkX7TbhIrnvzNO8e8m2wE 26 | +6F68/peYt2ERGL7MX7hqgW/Y6xRENZVXs7YBR/ifq/tK4qIfAO7y1GU9Ib2zB0iNYe3 27 | quz6oNJxj9CNrIU8BG7E7otYnsxbxws5ITHn79Bad8fV3Dg6BwjI1sYX+5p+nRUneKs8 28 | MC3g== 29 | X-Received: by 10.52.92.135 with SMTP id cm7mr8399923vdb.36.1372017246848; 30 | Sun, 23 Jun 2013 12:54:06 -0700 (PDT) 31 | X-Received: by 10.52.92.135 with SMTP id cm7mr8399918vdb.36.1372017246524; 32 | Sun, 23 Jun 2013 12:54:06 -0700 (PDT) 33 | Return-Path: 34 | Received: from mail-ve0-x22c.google.com (mail-ve0-x22c.google.com [2607:f8b0:400c:c01::22c]) 35 | by mx.google.com with ESMTPS id n7si4160348vcj.5.2013.06.23.12.54.06 36 | for 37 | (version=TLSv1 cipher=ECDHE-RSA-RC4-SHA bits=128/128); 38 | Sun, 23 Jun 2013 12:54:06 -0700 (PDT) 39 | Received-SPF: pass (google.com: domain of bslatkin@gmail.com designates 2607:f8b0:400c:c01::22c as permitted sender) client-ip=2607:f8b0:400c:c01::22c; 40 | Received: by mail-ve0-f172.google.com with SMTP id jz10so8076107veb.31 41 | for ; Sun, 23 Jun 2013 12:54:06 -0700 (PDT) 42 | DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; 43 | d=gmail.com; s=20120113; 44 | h=mime-version:date:message-id:subject:from:to:content-type; 45 | bh=CZnsNv0b4VreQhSqIaKaPPbnHlllv0UQlqvG8YKbXgU=; 46 | b=fSH2BsAr4Aiq9c4U7SzlZqHEm1xhJwqwXavkJWJOtJwba338WvGVOHwyVn9OF1sXwE 47 | e4RyCmqg4PkTDcqn43FbkXHiWX4VaN3D74IhW0Dht8CeX3tfmJhAYYk+FBUsCA1LBy/k 48 | iwb3AAoSq9a4uqShb05I1HhmiJ6d8QYOla+9ynVvNgXW2XArtsA8/D/h/z3CLZ+Vz2+7 49 | nd4IFgAF16UZsTPjre9Rnv6xcg3YPYToCAntKg0usVwjpBRm3k2ag2JXjgycZEmcLnQb 50 | OvbC8E/C0ie+U5oyd7kyz0RCG4tjV+KedX1a58lLrH2LnOLL3pdEOQAI6mHS1P1anjaA 51 | 3jdA== 52 | MIME-Version: 1.0 53 | X-Received: by 10.58.235.69 with SMTP id uk5mr10086631vec.17.1372017246038; 54 | Sun, 23 Jun 2013 12:54:06 -0700 (PDT) 55 | Received: by 10.58.251.136 with HTTP; Sun, 23 Jun 2013 12:54:06 -0700 (PDT) 56 | Date: Sun, 23 Jun 2013 12:54:06 -0700 57 | Message-ID: 58 | Subject: Testing this 59 | From: Brett Slatkin 60 | To: Brad Fitzpatrick 61 | Content-Type: multipart/alternative; boundary=047d7bd6c2c49372d204dfd7a867 62 | Delivered-To: brad@danga.com 63 | X-Gm-Message-State: ALoCoQlZDQw6ep0Z9Zm5hOrpnhJeDkvvRT93fWL2DDAHLu7OdN1trS+4VZ/2vmahlh0wt//VqSn5fRqaFUB2i9J5Ol91xaDqK2ev3DwZhZMluZVZTk8mv6w= 64 | 65 | --047d7bd6c2c49372d204dfd7a867 66 | Content-Type: text/plain; charset=ISO-8859-1 67 | 68 | webfist = http://www.example.com/foo/bar/baz.json 69 | 70 | --047d7bd6c2c49372d204dfd7a867 71 | Content-Type: text/html; charset=ISO-8859-1 72 | Content-Transfer-Encoding: quoted-printable 73 | 74 | 79 | 80 | --047d7bd6c2c49372d204dfd7a867-- 81 | -------------------------------------------------------------------------------- /testdata/twitter_dkim.txt: -------------------------------------------------------------------------------- 1 | Delivered-To: bslatkin@gmail.com 2 | Received: by 10.58.251.136 with SMTP id zk8csp108878vec; 3 | Sat, 22 Jun 2013 10:23:01 -0700 (PDT) 4 | X-Received: by 10.180.205.200 with SMTP id li8mr2004089wic.15.1371921781381; 5 | Sat, 22 Jun 2013 10:23:01 -0700 (PDT) 6 | Return-Path: 7 | Received: from mail-ve0-x229.google.com (mail-ve0-x229.google.com [2607:f8b0:400c:c01::229]) 8 | by mx.google.com with ESMTPS id gl9si1153805wic.3.2013.06.22.10.23.00 9 | for 10 | (version=TLSv1 cipher=ECDHE-RSA-RC4-SHA bits=128/128); 11 | Sat, 22 Jun 2013 10:23:01 -0700 (PDT) 12 | Received-SPF: neutral (google.com: 2607:f8b0:400c:c01::229 is neither permitted nor denied by best guess record for domain of brett+caf_=bslatkin=gmail.com@haxor.com) client-ip=2607:f8b0:400c:c01::229; 13 | Authentication-Results: mx.google.com; 14 | spf=neutral (google.com: 2607:f8b0:400c:c01::229 is neither permitted nor denied by best guess record for domain of brett+caf_=bslatkin=gmail.com@haxor.com) smtp.mail=brett+caf_=bslatkin=gmail.com@haxor.com; 15 | dkim=pass header.i=@twitter.com; 16 | dmarc=pass (p=REJECT dis=NONE) d=postmaster.twitter.com 17 | Received: by mail-ve0-x229.google.com with SMTP id m1so7530078ves.28 18 | for ; Sat, 22 Jun 2013 10:23:00 -0700 (PDT) 19 | X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; 20 | d=google.com; s=20120113; 21 | h=x-forwarded-to:x-forwarded-for:delivered-to:message-id 22 | :dkim-signature:x-msfbl:date:from:to:subject:mime-version 23 | :content-type:x-gm-message-state; 24 | bh=ahVpnSUOblFmZGj1rFaHzQPqFcRa386FZ89sMjjl7w0=; 25 | b=RQ+NlPOPKTMvHXQMmJS0bs48/5SfEcmF+5Q6ibGo3LbIpvMdLehdBZN7TMtkg7CC92 26 | nSC5F5F4V+PxHZ2Snt/Cuglbvz2Ph8JD0aZxPFw1IRxN307B0kdL3RGaelNjzk4Hh2QL 27 | IhUjF5F2cWysQXpqkBxAhmwagXc6ybm9AsnKvhFBWHrcB/L/XyoYhawtB4V2imP89XbG 28 | 0tO49j5AeO+Oasfg1jw82ghri82VXgq7XIgwr7p+nII79nYaNJ3w9m1ffQqywSvAZhFd 29 | S05gOWkPBlCtv1SR3MySFFYJlO6hoz3IPLv7Rug0UrWpDWEZIGRKY1R3VIX4t43czI2m 30 | E2sw== 31 | X-Received: by 10.220.7.134 with SMTP id d6mr8082838vcd.56.1371921780662; 32 | Sat, 22 Jun 2013 10:23:00 -0700 (PDT) 33 | X-Forwarded-To: bslatkin@gmail.com 34 | X-Forwarded-For: brett@haxor.com bslatkin@gmail.com 35 | Delivered-To: brett+webfist@haxor.com 36 | Received: by 10.58.243.198 with SMTP id xa6csp71506vec; 37 | Sat, 22 Jun 2013 10:23:00 -0700 (PDT) 38 | X-Received: by 10.236.198.206 with SMTP id v54mr9757165yhn.226.1371921780141; 39 | Sat, 22 Jun 2013 10:23:00 -0700 (PDT) 40 | Return-Path: 41 | Received: from spring-chicken-az.twitter.com (spring-chicken-az.twitter.com. [199.16.156.165]) 42 | by mx.google.com with ESMTP id l27si5730655yhm.70.2013.06.22.10.22.59 43 | for ; 44 | Sat, 22 Jun 2013 10:23:00 -0700 (PDT) 45 | Received-SPF: pass (google.com: domain of z077e0c878oergg+jrosvfg=unkbe.pbz@bounce.twitter.com designates 199.16.156.165 as permitted sender) client-ip=199.16.156.165; 46 | Message-Id: <51c5dd74.a7b5ec0a.12f3.fffff525SMTPIN_ADDED_MISSING@mx.google.com> 47 | DKIM-Signature: v=1; a=rsa-sha1; d=twitter.com; s=dkim-201303; c=relaxed/relaxed; 48 | q=dns/txt; i=@twitter.com; t=1371921779; 49 | h=From:Subject:Date:To; 50 | bh=KLyh4U2CsdVLGoKtohQAndfKVIM=; 51 | b=l7UrnzHW8Eu49ObP79fqbXrfJSKkZEqW0FPjsW/Wmmxc9zlEU9YrENmiPU51Mo+v 52 | TSJnBe/IIS8hUwfQP+5024i0KTtHobPM1PSZYhsCPha9IlHaZ8mJNlRJUVfK9z3/ 53 | pldHjRprxmYM8hbxJlAl89GfiqqoeFPI/aIxPjPEOvc=; 54 | X-MSFBL: YnJldHQrd2ViZmlzdEBoYXhvci5jb21AYXRsYS1hcWgtMzgtc3IxLUV2ZXJ5dGhp 55 | bmcuMTg1QEV2ZXJ5dGhpbmdA 56 | Date: Sat, 22 Jun 2013 17:22:59 +0000 57 | From: "Elias Mogshack (Twitter)" 58 | To: WebFist 59 | Subject: Elias Mogshack (@EliasMogshack) mentioned you on Twitter! 60 | MIME-Version: 1.0 61 | Content-Type: multipart/alternative; 62 | boundary="----=_Part_17886521_371525100.1371921779217" 63 | X-Gm-Message-State: ALoCoQkCeP19/+aXu67QRngaf1rFn9Qs5+Fqm4TL3A3InJOqRxWUtaQZ78sJpgl14H2GeXL7Xy37 64 | 65 | ------=_Part_17886521_371525100.1371921779217 66 | Content-Type: text/plain; charset=UTF-8 67 | Content-Transfer-Encoding: 7bit 68 | 69 | Elias Mogshack @EliasMogshack 70 | @webfist beep 71 | 09:22 AM - 22 Jun 13 72 | ------------------------ 73 | 74 | Keep the conversation going: 75 | Reply to @Elias Mogshack 76 | https://twitter.com/EliasMogshack/status/348491316075831296 77 | 78 | ------------------------ 79 | 80 | If you believe Elias Mogshack is engaging in abusive behavior on Twitter, you may report Elias Mogshack for spam: https://twitter.com/i/redirect?url=https%3A%2F%2Ftwitter.com%2Fuser_spam_reports%2FWebFist%2Freport%2FEliasMogshack%3Ft%3D1%26sig%3D36858a0325e44ca02bffacac5569c4afe618caf7%26iid%3Dc757f258-c99e-4fda-8538-8ed4beca4234%26uid%3D1539094274%26accused%3DEliasMogshack%26nid%3D4%2B694&sig=8bf22cf121a874b62e2428c9a055d0827e3f9e3b&uid=1539094274&iid=c757f258-c99e-4fda-8538-8ed4beca4234&nid=4+694&t=1 81 | Forgot your Twitter password? Get instructions on how to reset it: 82 | https://twitter.com/account/resend_password 83 | 84 | You can also unsubscribe from these emails or change your notification settings: https://twitter.com/i/u?t=1&sig=0e21c1493576ece531d4c73527ae4d078dae6866&iid=c757f258-c99e-4fda-8538-8ed4beca4234&uid=1539094274&nid=4+26 85 | https://twitter.com/settings/notifications 86 | 87 | Need help? 88 | https://support.twitter.com 89 | 90 | If you received this message in error and did not sign up for a Twitter account, click on the url below: 91 | https://twitter.com/account/not_my_account/WebFist/FD85B-C93A5-137192 92 | ------=_Part_17886521_371525100.1371921779217 93 | Content-Type: text/html; charset=UTF-8 94 | Content-Transfer-Encoding: 7bit 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 253 | 254 | 255 | @webfist beep - @EliasMogshack 256 | 257 | 258 | 259 | 488 | 489 | 490 |
260 | 261 | 262 | 263 | 294 | 295 | 296 | 297 | 451 | 452 | 453 | 485 | 486 | 487 |
264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 |
      272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 |
WebFist,
You were mentioned in a Tweet!
 
   
332 | 333 | 334 | 335 | 448 | 449 | 450 |
336 | 337 | 338 | 339 | 340 | 341 | 342 | 445 | 446 | 447 |
356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 |
Elias Mogshack
@WebFist beep
09:22 AM - 22 Jun 13
380 | 381 | 382 | 383 | 384 | 385 | 386 |
387 |
388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 439 | 440 | 441 | 442 | 443 | 444 |
396 | 397 | 398 | 399 | 434 | 435 | 436 | 437 | 438 |
418 | 419 | 420 | 421 | 431 | 432 | 433 |
422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 |
Reply to @EliasMogshack
Retweet Favorite
454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 478 | 479 | 480 | 481 | 482 | 483 | 484 |
491 | 492 | 493 | 494 | 495 | ------=_Part_17886521_371525100.1371921779217-- 496 | -------------------------------------------------------------------------------- /webfist.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2013 WebFist AUTHORS 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package webfist implements WebFist. 18 | package webfist 19 | 20 | import ( 21 | "time" 22 | ) 23 | 24 | // Storage is the interface implemented by backends. 25 | type Storage interface { 26 | PutEmail(*EmailAddr, *Email) error 27 | Emails(*EmailAddr) ([]*Email, error) 28 | 29 | // StatEncryptedBlob returns the size of the encrypted blob on 30 | // disk. addrKey (the Email's HexKey) and encSHA1 (the SHA-1 31 | // of the encrypted email) are lowercase hex. The err will be 32 | // os.ErrNotExist if the file is doesn't exist. 33 | StatEncryptedEmail(addrKey, encSHA1 string) (size int, err error) 34 | 35 | // EncryptedEmail returns the encrypted email with for the 36 | // addrKey (the Email's HexKey) and encSHA1 (the SHA-1 of | 37 | // fi, err := os.Stat(s.hexPath(sha1)) the encrypted 38 | // email). Both are lowercase hex. The err will be 39 | // os.ErrNotExist if the file is doesn't exist. 40 | EncryptedEmail(addrKey, sha1 string) ([]byte, error) 41 | 42 | PutEncryptedEmail(addrKey, sha1 string, data []byte) error 43 | 44 | // RecentMeta returns the recently-received encrypted emails. 45 | RecentMeta() ([]*RecentMeta, error) 46 | } 47 | 48 | // RecentMeta describes an encrypted email in the storage system. 49 | type RecentMeta struct { 50 | AddrHexKey string 51 | EncSHA1 string 52 | AddTime time.Time 53 | } 54 | 55 | // Defined in: http://tools.ietf.org/html/draft-ietf-appsawg-webfinger 56 | type Link struct { 57 | Rel string `json:"rel"` 58 | Type string `json:"type,omitempty"` 59 | Href string `json:"href"` 60 | Titles []string `json:"titles,omitempty"` 61 | Properties map[string]string `json:"properties,omitempty"` 62 | } 63 | 64 | type WebFingerResponse struct { 65 | Subject string `json:"subject"` 66 | Aliases []string `json:"aliases,omitempty"` 67 | Properties map[string]string `json:"properties,omitempty"` 68 | Links []Link `json:"links,omitempty"` 69 | } 70 | 71 | // Lookup performs a WebFinger query for an email address and returns all known 72 | // data for that address. Implementations may do standard WebFinger lookups over 73 | // the network, fallback to using the WebFist network, or use local storage to 74 | // map email address to WebFinger response. 75 | type Lookup interface { 76 | WebFinger(string) (*WebFingerResponse, error) 77 | } 78 | -------------------------------------------------------------------------------- /webfist_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2013 WebFist AUTHORS 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package webfist 18 | 19 | import ( 20 | "bytes" 21 | "io" 22 | "testing" 23 | ) 24 | 25 | func TestEmailKey(t *testing.T) { 26 | key := NewEmailAddr("brad@danga.com").HexKey() 27 | if want := "f888951d2ddcad78ffebce4a2c3158ecd1a60db0811a924ae7f41204828937c3"; key != want { 28 | t.Errorf("key = %q; want %q", key, want) 29 | } 30 | } 31 | 32 | func TestEncrypt(t *testing.T) { 33 | const msg = "From: foo\r\nTo: bar\r\n" 34 | email := NewEmailAddr("brad@danga.com") 35 | 36 | var encBuf bytes.Buffer 37 | enc := email.Encrypter(&encBuf) 38 | enc.Write([]byte(msg)) 39 | 40 | if encBuf.String() != "\xcd\xe2\x136n\xbe\xd4c\xf0\xefy4\xc5T\xe6\xda5o\x865" { 41 | t.Errorf("Encrypted value doesn't match what's expected.") 42 | } 43 | 44 | var decBuf bytes.Buffer 45 | io.Copy(&decBuf, email.Decrypter(&encBuf)) 46 | if decBuf.String() != msg { 47 | t.Errorf("Decrypted = %q; want %q", decBuf.String(), msg) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /webfistd/add.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2013 WebFist AUTHORS 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "fmt" 21 | "net/http" 22 | "strings" 23 | 24 | "github.com/bradfitz/webfist" 25 | ) 26 | 27 | func (s *server) WebFormAdd(w http.ResponseWriter, r *http.Request) { 28 | if r.Method != "POST" { 29 | http.Error(w, "Invalid method.", 400) 30 | return 31 | } 32 | all := r.FormValue("email") 33 | all = strings.TrimLeft(all, " \t\n\r") 34 | em, err := webfist.NewEmail([]byte(all)) 35 | if err != nil { 36 | http.Error(w, "Bogus email: " + err.Error(), 400) 37 | return 38 | } 39 | 40 | from, err := em.From() 41 | if err != nil { 42 | http.Error(w, "No From", 400) 43 | return 44 | } 45 | 46 | if !em.Verify() { 47 | http.Error(w, "Email didn't verify. No DKIM.", 400) 48 | return 49 | } 50 | 51 | webfist, err := em.WebFist() 52 | if err != nil { 53 | http.Error(w, "Email didn't contain WebFist command: " + err.Error(), 400) 54 | return 55 | } 56 | 57 | err = s.storage.PutEmail(from, em) 58 | if err != nil { 59 | http.Error(w, "Storage error: " + err.Error(), 500) 60 | return 61 | } 62 | fmt.Fprintf(w, "Saved. Extracted email = %#v", webfist) 63 | } 64 | -------------------------------------------------------------------------------- /webfistd/blob.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2013 WebFist AUTHORS 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "bytes" 21 | "io/ioutil" 22 | "log" 23 | "net/http" 24 | "path" 25 | "strings" 26 | 27 | "github.com/bradfitz/webfist" 28 | ) 29 | 30 | func (s *server) ServeBlob(w http.ResponseWriter, r *http.Request) { 31 | if r.ParseForm() != nil { 32 | log.Printf("Could not parse form: %+v", r) 33 | http.Error(w, "Bad request", http.StatusBadRequest) 34 | return 35 | } 36 | 37 | base := path.Base(r.URL.Path) 38 | parts := strings.SplitN(base, "-", 2) 39 | if len(parts) != 2 { 40 | log.Printf("Invalid blob path: %q", r.URL.Path) 41 | http.Error(w, "Bad request", http.StatusBadRequest) 42 | return 43 | } 44 | 45 | hexKey, encSHA1 := parts[0], parts[1] 46 | 47 | all, err := s.storage.EncryptedEmail(hexKey, encSHA1) 48 | if err != nil { 49 | http.NotFound(w, r) 50 | return 51 | } 52 | 53 | decryptKey := r.Form.Get("decrypt") 54 | if decryptKey == "" { 55 | w.Header().Add("Content-Type", "application/octet-stream") 56 | w.Write(all) 57 | return 58 | } 59 | 60 | addr := webfist.NewEmailAddr(decryptKey) 61 | decrypted, err := ioutil.ReadAll(addr.Decrypter(bytes.NewReader(all))) 62 | if err != nil { 63 | http.Error(w, "Could not decrypt blob", http.StatusInternalServerError) 64 | return 65 | } 66 | 67 | w.Header().Add("Content-Type", "text/plain; charset=utf-8") 68 | w.Header().Add("Access-Control-Allow-Origin", "*") 69 | w.Write(decrypted) 70 | } 71 | -------------------------------------------------------------------------------- /webfistd/front.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2013 WebFist AUTHORS 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "fmt" 21 | "net/http" 22 | ) 23 | 24 | func init() { 25 | http.HandleFunc("/", front) 26 | } 27 | 28 | func front(w http.ResponseWriter, r *http.Request) { 29 | fmt.Fprintf(w, ` 30 | 31 | 32 | 33 | WebFist 34 | 58 | 59 | 60 |
61 |

WebFist!

62 |

This is a WebFist server. See the introduction & overview blog post and the source at github.com/bradfitz/webfist. You 64 | can run your own WebFist server and join the fist-bump network. Use WebFist 65 | servers to do WebFinger look-ups when the canonical reply (from the domain that 66 | owns an email address) is invalid or missing.

67 |

Send email to fist@webfist.org 68 | to set your WebFinger delegation. The email must be DKIM-signed. You will not 69 | receive a response email. The contents of the email should be like this. The URL 70 | you point to should be a JRD document 71 | defined by the WebFinger spec.

72 |
73 | webfist = http://example.com/path/to/your-profile 74 |
75 |
76 |
77 |

78 | Lookup your email address using WebFist: 79 |

80 | 81 | 82 |
83 |

84 |
85 |

Or, send yourself an email and paste the full email headers and body here: 86 |

87 |
88 | 89 |
90 | [Recent changes] 91 |
92 |

93 |
94 |
95 | 96 | 97 | `) 98 | } 99 | -------------------------------------------------------------------------------- /webfistd/localdisk.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2013 WebFist AUTHORS 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "strings" 21 | "crypto/sha1" 22 | "errors" 23 | "fmt" 24 | "io/ioutil" 25 | "os" 26 | "path/filepath" 27 | "regexp" 28 | "sort" 29 | 30 | "github.com/bradfitz/webfist" 31 | ) 32 | 33 | const recentCount = 1000 34 | 35 | type diskStorage struct { 36 | root string 37 | recentDir string 38 | } 39 | 40 | func NewDiskStorage(root string) (webfist.Storage, error) { 41 | ds := &diskStorage{ 42 | root: root, 43 | } 44 | ds.recentDir = filepath.Join(root, "recent") 45 | if err := os.MkdirAll(ds.recentDir, 0700); err != nil { 46 | return nil, err 47 | } 48 | go ds.cleanRecent() 49 | return ds, nil 50 | } 51 | 52 | func (s *diskStorage) RecentMeta() (rm []*webfist.RecentMeta, err error) { 53 | f, err := os.Open(s.recentDir) 54 | if err != nil { 55 | return 56 | } 57 | defer f.Close() 58 | fis, err := f.Readdir(-1) 59 | if err != nil { 60 | return 61 | } 62 | sort.Sort(byModTime(fis)) 63 | for _, fi := range fis { 64 | name := fi.Name() 65 | if !strings.HasSuffix(name, ".recent") { 66 | continue 67 | } 68 | name = strings.TrimSuffix(name, ".recent") 69 | s := strings.SplitN(name, "-", 2) 70 | if len(s) != 2 { 71 | continue 72 | } 73 | rm = append(rm, &webfist.RecentMeta{ 74 | AddrHexKey: s[0], 75 | EncSHA1: s[1], 76 | AddTime: fi.ModTime(), 77 | }) 78 | } 79 | return rm, nil 80 | } 81 | 82 | func (s *diskStorage) cleanRecent() error { 83 | f, err := os.Open(s.recentDir) 84 | if err != nil { 85 | return err 86 | } 87 | defer f.Close() 88 | fis, err := f.Readdir(-1) 89 | if err != nil { 90 | return err 91 | } 92 | if len(fis) <= recentCount { 93 | return nil 94 | } 95 | sort.Sort(byModTime(fis)) 96 | toDelete := fis[:len(fis)-recentCount] 97 | for _, fi := range toDelete { 98 | path := filepath.Join(s.recentDir, filepath.Base(fi.Name())) 99 | os.Remove(path) 100 | } 101 | return nil 102 | } 103 | 104 | type byModTime []os.FileInfo 105 | 106 | func (s byModTime) Len() int { return len(s) } 107 | func (s byModTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 108 | func (s byModTime) Less(i, j int) bool { return s[i].ModTime().Before(s[j].ModTime()) } 109 | 110 | func (s *diskStorage) emailRootFromHex(addrHexKey string) string { 111 | x := addrHexKey 112 | if len(x) < 7 { 113 | panic("bogus emailRootFromHex") 114 | } 115 | return filepath.Join(s.root, x[:3], x[3:6], x) 116 | } 117 | 118 | func (s *diskStorage) emailRoot(addr *webfist.EmailAddr) string { 119 | return s.emailRootFromHex(addr.HexKey()) 120 | } 121 | 122 | var ( 123 | rxAddrKey = regexp.MustCompile(`^[0-9a-f]{7,}$`) 124 | rxSHA1 = regexp.MustCompile(`^[0-9a-f]{40,40}$`) 125 | errInvalidBlobref = errors.New("Invalid sha1") 126 | ) 127 | 128 | func (s *diskStorage) encFile(addrKey, encSHA1 string) (string, error) { 129 | if !rxAddrKey.MatchString(addrKey) || !rxSHA1.MatchString(encSHA1) { 130 | return "", errInvalidBlobref 131 | } 132 | return filepath.Join(s.emailRootFromHex(addrKey), encSHA1), nil 133 | } 134 | 135 | func (s *diskStorage) StatEncryptedEmail(addrKey, encSHA1 string) (size int, err error) { 136 | path, err := s.encFile(addrKey, encSHA1) 137 | if err != nil { 138 | return 139 | } 140 | fi, err := os.Stat(path) 141 | if err != nil { 142 | return 143 | } 144 | return int(fi.Size()), nil 145 | } 146 | 147 | func (s *diskStorage) EncryptedEmail(addrKey, encSHA1 string) ([]byte, error) { 148 | path, err := s.encFile(addrKey, encSHA1) 149 | if err != nil { 150 | return nil, err 151 | } 152 | return ioutil.ReadFile(path) 153 | } 154 | 155 | func (s *diskStorage) touchRecent(addrKey, encSHA1 string) error { 156 | // Touch the recent file. 157 | err := ioutil.WriteFile(filepath.Join(s.recentDir, addrKey+"-"+encSHA1+".recent"), nil, 0600) 158 | if err == nil { 159 | go s.cleanRecent() 160 | } 161 | return err 162 | } 163 | 164 | func (s *diskStorage) PutEncryptedEmail(addrKey, encSHA1 string, data []byte) error { 165 | s1 := sha1.New() 166 | s1.Write(data) 167 | if fmt.Sprintf("%x", s1.Sum(nil)) != encSHA1 { 168 | return errInvalidBlobref 169 | } 170 | emailRoot := s.emailRootFromHex(addrKey) 171 | err := os.MkdirAll(emailRoot, 0755) 172 | if err != nil { 173 | return err 174 | } 175 | emailPath := filepath.Join(emailRoot, encSHA1) 176 | if err := ioutil.WriteFile(emailPath, data, 0644); err != nil { 177 | return err 178 | } 179 | return s.touchRecent(addrKey, encSHA1) 180 | } 181 | 182 | func (s *diskStorage) PutEmail(addr *webfist.EmailAddr, email *webfist.Email) error { 183 | emailRoot := s.emailRoot(addr) 184 | err := os.MkdirAll(emailRoot, 0755) 185 | if err != nil { 186 | return err 187 | } 188 | 189 | r, err := email.Encrypted() 190 | if err != nil { 191 | return err 192 | } 193 | enc, err := ioutil.ReadAll(r) 194 | if err != nil { 195 | return err 196 | } 197 | 198 | s1 := sha1.New() 199 | s1.Write(enc) 200 | 201 | addrKey := addr.HexKey() 202 | encSHA1 := fmt.Sprintf("%x", s1.Sum(nil)) 203 | email.SetEncSHA1(encSHA1) 204 | 205 | emailPath := filepath.Join(emailRoot, encSHA1) 206 | if err := ioutil.WriteFile(emailPath, enc, 0644); err != nil { 207 | return err 208 | } 209 | return s.touchRecent(addrKey, encSHA1) 210 | } 211 | 212 | func (s *diskStorage) Emails(addr *webfist.EmailAddr) ([]*webfist.Email, error) { 213 | emailRoot := s.emailRoot(addr) 214 | file, err := os.Open(emailRoot) 215 | if os.IsNotExist(err) { 216 | return nil, nil 217 | } 218 | if err != nil { 219 | return nil, err 220 | } 221 | infoList, err := file.Readdir(-1) 222 | if err != nil { 223 | return nil, err 224 | } 225 | result := make([]*webfist.Email, len(infoList)) 226 | for i, info := range infoList { 227 | emailPath := filepath.Join(emailRoot, info.Name()) 228 | file, err := os.Open(emailPath) 229 | if err != nil { 230 | return nil, err 231 | } 232 | all, err := ioutil.ReadAll(addr.Decrypter(file)) 233 | if err != nil { 234 | return nil, err 235 | } 236 | email, err := webfist.NewEmail(all) 237 | if err != nil { 238 | return nil, err 239 | } 240 | result[i] = email 241 | } 242 | return result, nil 243 | } 244 | -------------------------------------------------------------------------------- /webfistd/lookup.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2013 WebFist AUTHORS 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "log" 23 | "net/http" 24 | "net/url" 25 | "sort" 26 | "strings" 27 | 28 | "github.com/bradfitz/webfist" 29 | ) 30 | 31 | func (s *server) Lookup(w http.ResponseWriter, r *http.Request) { 32 | if r.ParseForm() != nil { 33 | http.Error(w, "Bad request", http.StatusBadRequest) 34 | return 35 | } 36 | resource := r.Form.Get("resource") 37 | if resource == "" { 38 | http.Error(w, "'resource' missing", http.StatusBadRequest) 39 | return 40 | } 41 | emailLikeId := strings.TrimPrefix(resource, "acct:") 42 | if emailLikeId == resource { 43 | http.Error(w, "'resource' must start with 'acct:'", http.StatusBadRequest) 44 | return 45 | } 46 | 47 | foundData, err := s.lookup.WebFinger(emailLikeId) 48 | if err != nil { 49 | log.Printf("Error looking up resource: %s -- %v", emailLikeId, err) 50 | http.Error(w, "Error doing lookup", http.StatusInternalServerError) 51 | return 52 | } 53 | if foundData == nil { 54 | log.Printf("Not found: %s", emailLikeId) 55 | http.Error(w, "No WebFist data found for that resource", 404) 56 | return 57 | } 58 | b, err := json.Marshal(foundData) 59 | if err != nil { 60 | log.Printf("Bad data for resource: %s -- %v", emailLikeId, err) 61 | http.Error(w, "Bad data from lookup", http.StatusInternalServerError) 62 | return 63 | } 64 | 65 | log.Printf("Found user %s -- %+v", emailLikeId, foundData) 66 | w.Header().Add("Content-Type", "application/json; charset=utf-8") 67 | w.Header().Add("Access-Control-Allow-Origin", "*") 68 | w.Write(b) 69 | } 70 | 71 | type emailLookup struct { 72 | storage webfist.Storage 73 | } 74 | 75 | type byEmailDate []*webfist.Email 76 | 77 | 78 | func (s byEmailDate) Len() int { 79 | return len(s) 80 | } 81 | 82 | func (s byEmailDate) Swap(i, j int) { 83 | s[i], s[j] = s[j], s[i] 84 | } 85 | 86 | func (s byEmailDate) Less(i, j int) bool { 87 | d1, err := s[i].Date() 88 | if err != nil { 89 | return false 90 | } 91 | d2, err := s[j].Date() 92 | if err != nil { 93 | return false 94 | } 95 | return d1.Before(d2) 96 | } 97 | 98 | func (l *emailLookup) WebFinger(addr string) (*webfist.WebFingerResponse, error) { 99 | emailAddr := webfist.NewEmailAddr(addr) 100 | emailList, err := l.storage.Emails(emailAddr) 101 | if err != nil { 102 | return nil, err 103 | } 104 | if len(emailList) == 0 { 105 | return nil, nil 106 | } 107 | sort.Sort(byEmailDate(emailList)) 108 | lastEmail := emailList[len(emailList) - 1] 109 | // TODO: Garbage collect old emails 110 | 111 | delegationURL, err := lastEmail.WebFist() 112 | if err != nil { 113 | return nil, err 114 | } 115 | encSHA1, err := lastEmail.EncSHA1() 116 | if err != nil { 117 | return nil, err 118 | } 119 | proofURL := fmt.Sprintf("%s/webfist/proof/%s-%s?decrypt=%s", *baseURL, emailAddr.HexKey(), encSHA1, url.QueryEscape(emailAddr.Canonical())) 120 | 121 | resp := &webfist.WebFingerResponse { 122 | Subject: emailAddr.Canonical(), 123 | Links: []webfist.Link { 124 | { 125 | Rel: "http://webfist.org/spec/rel", 126 | Href: delegationURL, 127 | Properties: map[string]string { 128 | "http://webfist.org/spec/proof": proofURL, 129 | }, 130 | }, 131 | }, 132 | } 133 | return resp, nil 134 | } 135 | 136 | func NewLookup(storage webfist.Storage) webfist.Lookup { 137 | return &emailLookup{ 138 | storage: storage, 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /webfistd/lookup_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2013 WebFist AUTHORS 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "io/ioutil" 21 | "net/http" 22 | "net/http/httptest" 23 | "path/filepath" 24 | "testing" 25 | 26 | "github.com/bradfitz/webfist" 27 | ) 28 | 29 | type DummyStorage struct{} 30 | 31 | func (DummyStorage) PutEmail(*webfist.EmailAddr, *webfist.Email) error { 32 | return nil 33 | } 34 | 35 | func (DummyStorage) Emails(*webfist.EmailAddr) ([]*webfist.Email, error) { 36 | files := []string{ 37 | "gmail_dkim.txt", 38 | } 39 | var res []*webfist.Email 40 | for _, file := range files { 41 | full := filepath.Join("../testdata", file) 42 | all, err := ioutil.ReadFile(full) 43 | if err != nil { 44 | return nil, err 45 | } 46 | e, err := webfist.NewEmail(all) 47 | if err != nil { 48 | return nil, err 49 | } 50 | res = append(res, e) 51 | } 52 | return res, nil 53 | } 54 | 55 | func (DummyStorage) StatEncryptedEmail(addrKey, encSHA1 string) (size int, err error) { 56 | panic("Not implemented") 57 | } 58 | 59 | func (DummyStorage) EncryptedEmail(addrKey, sha1 string) ([]byte, error) { 60 | panic("Not implemented") 61 | } 62 | 63 | func (DummyStorage) PutEncryptedEmail(addrKey, encSHA1 string, data []byte) error { 64 | panic("Not implemented") 65 | } 66 | 67 | func (DummyStorage) RecentMeta() ([]*webfist.RecentMeta, error) { 68 | panic("Not implemented") 69 | } 70 | 71 | var ( 72 | testServer *server 73 | ) 74 | 75 | func init() { 76 | storage := &DummyStorage{} 77 | testServer = &server{ 78 | storage: storage, 79 | lookup: NewLookup(storage), 80 | } 81 | } 82 | 83 | func TestEmailLookup(t *testing.T) { 84 | req, _ := http.NewRequest("GET", "http://example.com/foo?resource=acct:myname@example.com", nil) 85 | resp := httptest.NewRecorder() 86 | testServer.Lookup(resp, req) 87 | body := resp.Body.String() 88 | wants := `{"subject":"myname@example.com","links":[{"rel":"http://webfist.org/spec/rel","href":"http://www.example.com/foo/bar/baz.json","properties":{"http://webfist.org/spec/proof":"http://webfist.org/webfist/proof/9239956c3d0668d7d0009ef14228bfbbc43dfd10-3a3202736e2f25cae0f5acfb011b6436eb28e27d?decrypt=myname%40example.com"}}]}` 89 | if body != wants { 90 | t.Fatalf("Body = %q; want %q", body, wants) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /webfistd/recent.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2013 WebFist AUTHORS 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "bytes" 21 | "fmt" 22 | "net/http" 23 | "time" 24 | ) 25 | 26 | func (s *server) ServeRecent(w http.ResponseWriter, r *http.Request) { 27 | rm, err := s.storage.RecentMeta() 28 | if err != nil { 29 | http.Error(w, err.Error(), 500) 30 | return 31 | } 32 | var max time.Time 33 | var buf bytes.Buffer 34 | for _, m := range rm { 35 | if m.AddTime.After(max) { 36 | max = m.AddTime 37 | } 38 | fmt.Fprintf(&buf, "%s %s-%s\n", m.AddTime.Format(time.RFC3339), m.AddrHexKey, m.EncSHA1) 39 | } 40 | w.Header().Add("Access-Control-Allow-Origin", "*") 41 | w.Header().Set("Content-Type", "text/plain; charset=utf-8") 42 | http.ServeContent(w, r, "", max, bytes.NewReader(buf.Bytes())) 43 | } 44 | -------------------------------------------------------------------------------- /webfistd/smtp.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2013 WebFist AUTHORS 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // SMTP-related parts of webfistd. 18 | 19 | package main 20 | 21 | import ( 22 | "bytes" 23 | "errors" 24 | "flag" 25 | "log" 26 | "net" 27 | "strings" 28 | "time" 29 | 30 | "github.com/bradfitz/go-smtpd/smtpd" 31 | "github.com/bradfitz/webfist" 32 | ) 33 | 34 | var hostName = flag.String("hostname", "webfist.org", "Hostname to announce over SMTP") 35 | 36 | var ( 37 | lf = []byte("\n") 38 | crlf = []byte("\r\n") 39 | dkimSignature = []byte("DKIM-Signature") 40 | ) 41 | 42 | func (s *server) initSMTPServer() { 43 | s.smtpServer = &smtpd.Server{ 44 | ReadTimeout: 5 * time.Minute, 45 | WriteTimeout: 5 * time.Minute, 46 | Hostname: *hostName, 47 | OnNewMail: s.onNewMail, 48 | } 49 | } 50 | 51 | func (s *server) onNewMail(conn smtpd.Connection, from smtpd.MailAddress) (smtpd.Envelope, error) { 52 | log.Printf("smtp: new mail from %s", from.Email()) 53 | return &env{s: s, from: webfist.NewEmailAddr(from.Email())}, nil 54 | } 55 | 56 | func (s *server) runSMTP(ln net.Listener) { 57 | err := s.smtpServer.Serve(ln) 58 | log.Fatalf("SMTP failure: %v", err) 59 | } 60 | 61 | type env struct { 62 | from *webfist.EmailAddr 63 | buf bytes.Buffer 64 | s *server 65 | headersDone bool 66 | } 67 | 68 | // hasSignatureHeaders true if e likely contains a signed email. 69 | // False positives are okay. 70 | func (e *env) hasSignatureHeader() bool { 71 | if bytes.Contains(e.buf.Bytes(), dkimSignature) { 72 | return true 73 | } 74 | if strings.Contains(strings.ToLower(e.buf.String()), "dkim-signature") { 75 | return true 76 | } 77 | return false 78 | } 79 | 80 | func (e *env) AddRecipient(rcpt smtpd.MailAddress) error { return nil } 81 | func (e *env) BeginData() error { return nil } 82 | 83 | func (e *env) Write(line []byte) error { 84 | if e.buf.Len()+len(line) > webfist.MaxEmailSize { 85 | return errors.New("email too large for webfist") 86 | } 87 | e.buf.Write(line) 88 | if !e.headersDone && (bytes.Equal(line, lf) || bytes.Equal(line, crlf)) { 89 | e.headersDone = true 90 | if !e.hasSignatureHeader() { 91 | log.Printf("Rejecting email that isn't signed.") 92 | return errors.New("not a signed email") 93 | } 94 | } 95 | return nil 96 | } 97 | 98 | func (e *env) Close() error { 99 | em, err := webfist.NewEmail(e.buf.Bytes()) 100 | if err != nil { 101 | return err 102 | } 103 | verified := em.Verify() 104 | log.Printf("email from %v; verified = %v", e.from.Canonical(), verified) 105 | if !verified { 106 | return errors.New("DKIM verification failed") 107 | } 108 | 109 | _, err = em.WebFist() 110 | if err != nil { 111 | return errors.New("Invalid or missing WebFist commands in email.") 112 | } 113 | from, err := em.From() 114 | if err != nil { 115 | return errors.New("Bogus From header") 116 | } 117 | 118 | return e.s.storage.PutEmail(from, em) 119 | } 120 | -------------------------------------------------------------------------------- /webfistd/sync.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2013 WebFist AUTHORS 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "bufio" 21 | "io" 22 | "io/ioutil" 23 | "log" 24 | "net/http" 25 | "strings" 26 | "time" 27 | "flag" 28 | ) 29 | 30 | var syncInterval = flag.Duration("sync_interval", 10 * time.Second, "Sync poll interval.") 31 | 32 | func (s *server) syncFromPeers() { 33 | for _, peer := range s.peers { 34 | go s.syncFromPeer(peer) 35 | } 36 | } 37 | 38 | func (srv *server) syncFromPeer(host string) { 39 | log.Printf("Starting sync from %v", host) 40 | var ims time.Time 41 | var sleep time.Duration 42 | for { 43 | time.Sleep(sleep) 44 | url := "http://"+host+"/webfist/bump" 45 | log.Printf("Doing request to %v", url) 46 | req, err := http.NewRequest("GET", url, nil) 47 | if err != nil { 48 | panic("Bogus host: " + host + ": " + err.Error()) 49 | } 50 | sleep = *syncInterval 51 | res, err := http.DefaultClient.Do(req) 52 | if err != nil { 53 | log.Printf("Fetch error from %s: %v", host, err) 54 | continue 55 | } 56 | sc := bufio.NewScanner(res.Body) 57 | for sc.Scan() { 58 | s := strings.Split(sc.Text(), " ") 59 | if len(s) != 2 { 60 | continue 61 | } 62 | modTime, err := time.Parse(time.RFC3339, s[0]) 63 | if err != nil { 64 | continue 65 | } 66 | if modTime.After(ims) { 67 | ims = modTime 68 | } 69 | s = strings.Split(s[1], "-") 70 | if len(s) != 2 { 71 | continue 72 | } 73 | addrHexKey, encSHA1 := s[0], s[1] 74 | _, err = srv.storage.StatEncryptedEmail(addrHexKey, encSHA1) 75 | if err != nil { 76 | log.Printf("Need to fetch %s-%s", addrHexKey, encSHA1) 77 | res, err := http.Get("http://" + host + "/webfist/proof/" + addrHexKey + "-" + encSHA1) 78 | if err != nil { 79 | log.Printf("Error fetching %s-%s: %v", addrHexKey, encSHA1, err) 80 | continue 81 | } 82 | slurp, err := ioutil.ReadAll(io.LimitReader(res.Body, 100<<10)) 83 | if err != nil { 84 | log.Printf("Error fetching %s-%s: %v", addrHexKey, encSHA1, err) 85 | continue 86 | } 87 | err = srv.storage.PutEncryptedEmail(addrHexKey, encSHA1, slurp) 88 | if err != nil { 89 | log.Printf("Error storing fetched %s-%s: %v", addrHexKey, encSHA1, err) 90 | continue 91 | } 92 | log.Printf("Synced %s-%s from %s", addrHexKey, encSHA1, host) 93 | } 94 | } 95 | if err := sc.Err(); err != nil { 96 | log.Printf("Scan error: %v", err) 97 | } 98 | res.Body.Close() 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /webfistd/webfistd.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2013 WebFist AUTHORS 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "log" 22 | "net/http" 23 | "os" 24 | "path/filepath" 25 | "runtime" 26 | "strings" 27 | 28 | "github.com/bradfitz/go-smtpd/smtpd" 29 | "github.com/bradfitz/runsit/listen" 30 | "github.com/bradfitz/webfist" 31 | ) 32 | 33 | var ( 34 | webAddr = listen.NewFlag("web", ":8080", "Web port") 35 | smtpAddr = listen.NewFlag("smtp", ":2500", "SMTP port") 36 | storageRoot = flag.String("root", "", "Root for local disk storage") 37 | baseURL = flag.String("base", "http://webfist.org", "Base URL without trailing slash for all server-side generated URLs.") 38 | peers = flag.String("peers", "", "Comma-separated list of hosts to replicate from.") 39 | ) 40 | 41 | type server struct { 42 | httpServer http.Server 43 | smtpServer *smtpd.Server 44 | lookup webfist.Lookup 45 | storage webfist.Storage 46 | peers []string // hosts 47 | } 48 | 49 | func main() { 50 | flag.Parse() 51 | 52 | webln, err := webAddr.Listen() 53 | if err != nil { 54 | log.Fatalf("web listen: %v", err) 55 | } 56 | smtpln, err := smtpAddr.Listen() 57 | if err != nil { 58 | log.Fatalf("SMTP listen: %v", err) 59 | } 60 | 61 | if *storageRoot == "" { 62 | varDir := "var" 63 | if runtime.GOOS == "darwin" { 64 | varDir = "Library" 65 | } 66 | *storageRoot = filepath.Join(os.Getenv("HOME"), varDir, "webfistd") 67 | if err := os.MkdirAll(*storageRoot, 0700); err != nil { 68 | log.Fatal(err) 69 | } 70 | } 71 | 72 | storage, err := NewDiskStorage(*storageRoot) 73 | if err != nil { 74 | log.Fatalf("Disk storage of %s: %v", *storageRoot, err) 75 | } 76 | srv := &server{ 77 | storage: storage, 78 | lookup: NewLookup(storage), 79 | } 80 | if *peers != "" { 81 | srv.peers = strings.Split(*peers, ",") 82 | } 83 | go srv.syncFromPeers() 84 | srv.initSMTPServer() 85 | log.Printf("Server up. web %s, smtp %s", webAddr, smtpAddr) 86 | go srv.runSMTP(smtpln) 87 | 88 | http.HandleFunc("/.well-known/webfinger", srv.Lookup) 89 | http.HandleFunc("/webfist/bump", srv.ServeRecent) 90 | http.HandleFunc("/webfist/proof/", srv.ServeBlob) 91 | http.HandleFunc("/add", srv.WebFormAdd) 92 | 93 | log.Fatal(srv.httpServer.Serve(webln)) 94 | } 95 | --------------------------------------------------------------------------------