├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── client.go ├── client_test.go ├── examples ├── cmdline │ ├── .gitignore │ ├── Makefile │ └── main.go └── simple │ └── main.go ├── jrd ├── parser.go └── parser_test.go ├── webfist.go └── webfist_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.test 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.1 4 | - 1.2.1 5 | - tip 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Antoine Imbert 2 | 3 | The MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Go-Webfinger 3 | ============ 4 | 5 | *Go client for the Webfinger protocol* 6 | 7 | [![Build Status](https://travis-ci.org/ant0ine/go-webfinger.png?branch=master)](https://travis-ci.org/ant0ine/go-webfinger) 8 | 9 | **Go-Webfinger** is a Go client for the Webfinger protocol. 10 | 11 | *It is a work in progress, the API is not frozen. 12 | We're trying to catchup with the last draft of the protocol: 13 | http://tools.ietf.org/html/draft-ietf-appsawg-webfinger-14 14 | and to support the http://webfist.org * 15 | 16 | Install 17 | ------- 18 | 19 | This package is "go-gettable", just do: 20 | 21 | go get github.com/ant0ine/go-webfinger 22 | 23 | Example 24 | ------- 25 | 26 | ~~~ go 27 | package main 28 | 29 | import ( 30 | "fmt" 31 | "github.com/ant0ine/go-webfinger" 32 | "os" 33 | ) 34 | 35 | func main() { 36 | email := os.Args[1] 37 | 38 | client := webfinger.NewClient(nil) 39 | 40 | resource, err := webfinger.MakeResource(email) 41 | if err != nil { 42 | panic(err) 43 | } 44 | 45 | jrd, err := client.GetJRD(resource) 46 | if err != nil { 47 | fmt.Println(err) 48 | return 49 | } 50 | 51 | fmt.Printf("JRD: %+v", jrd) 52 | } 53 | ~~~ 54 | 55 | Documentation 56 | ------------- 57 | 58 | - [Online Documentation (godoc.org)](http://godoc.org/github.com/ant0ine/go-webfinger) 59 | 60 | Author 61 | ------ 62 | - [Antoine Imbert](https://github.com/ant0ine) 63 | 64 | Contributors 65 | ------------ 66 | 67 | - Thanks [Will Norris](https://github.com/willnorris) for the major update to support draft-14, and the GAE compat! 68 | 69 | 70 | [MIT License](https://github.com/ant0ine/go-webfinger/blob/master/LICENSE) 71 | 72 | [![Analytics](https://ga-beacon.appspot.com/UA-309210-4/go-webfinger/readme)](https://github.com/igrigorik/ga-beacon) 73 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | // Package webfinger provides a simple client implementation of the WebFinger 2 | // protocol. 3 | // 4 | // It is a work in progress, the API is not frozen. 5 | // We're trying to catchup with the last draft of the protocol: 6 | // http://tools.ietf.org/html/draft-ietf-appsawg-webfinger-14 7 | // and to support the http://webfist.org 8 | // 9 | // Example: 10 | // 11 | // package main 12 | // 13 | // import ( 14 | // "fmt" 15 | // "github.com/ant0ine/go-webfinger" 16 | // "os" 17 | // ) 18 | // 19 | // func main() { 20 | // email := os.Args[1] 21 | // 22 | // client := webfinger.NewClient(nil) 23 | // 24 | // resource, err := webfinger.MakeResource(email) 25 | // if err != nil { 26 | // panic(err) 27 | // } 28 | // 29 | // jrd, err := client.GetJRD(resource) 30 | // if err != nil { 31 | // fmt.Println(err) 32 | // return 33 | // } 34 | // 35 | // fmt.Printf("JRD: %+v", jrd) 36 | // } 37 | package webfinger 38 | 39 | import ( 40 | "errors" 41 | "fmt" 42 | "io/ioutil" 43 | "log" 44 | "net/http" 45 | "net/url" 46 | "strings" 47 | 48 | "github.com/ant0ine/go-webfinger/jrd" 49 | ) 50 | 51 | // Resource is a resource for which a WebFinger query can be issued. 52 | type Resource url.URL 53 | 54 | // Parse parses rawurl into a WebFinger Resource. The rawurl should be an 55 | // absolute URL, or an email-like identifier (e.g. "bob@example.com"). 56 | func Parse(rawurl string) (*Resource, error) { 57 | u, err := url.Parse(rawurl) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | // if parsed URL has no scheme but is email-like, treat it as an acct: URL. 63 | if u.Scheme == "" { 64 | parts := strings.SplitN(rawurl, "@", 2) 65 | if len(parts) != 2 { 66 | return nil, fmt.Errorf("URL must be absolute, or an email address: %v", rawurl) 67 | } 68 | return Parse("acct:" + rawurl) 69 | } 70 | 71 | r := Resource(*u) 72 | return &r, nil 73 | } 74 | 75 | // WebFingerHost returns the default host for issuing WebFinger queries for 76 | // this resource. For Resource URLs with a host component, that value is used. 77 | // For URLs that do not have a host component, the host is determined by other 78 | // mains if possible (for example, the domain in the addr-spec of a mailto 79 | // URL). If the host cannot be determined from the URL, this value will be an 80 | // empty string. 81 | func (r *Resource) WebFingerHost() string { 82 | if r.Host != "" { 83 | return r.Host 84 | } else if r.Scheme == "acct" || r.Scheme == "mailto" { 85 | parts := strings.SplitN(r.Opaque, "@", 2) 86 | if len(parts) == 2 { 87 | return parts[1] 88 | } 89 | } 90 | return "" 91 | } 92 | 93 | // String reassembles the Resource into a valid URL string. 94 | func (r *Resource) String() string { 95 | u := url.URL(*r) 96 | return u.String() 97 | } 98 | 99 | // JRDURL returns the WebFinger query URL at the specified host for this 100 | // resource. If host is an empty string, the default host for the resource 101 | // will be used, as returned from WebFingerHost(). 102 | func (r *Resource) JRDURL(host string, rels []string) *url.URL { 103 | if host == "" { 104 | host = r.WebFingerHost() 105 | } 106 | 107 | return &url.URL{ 108 | Scheme: "https", 109 | Host: host, 110 | Path: "/.well-known/webfinger", 111 | RawQuery: url.Values{ 112 | "resource": []string{r.String()}, 113 | "rel": rels, 114 | }.Encode(), 115 | } 116 | } 117 | 118 | // A Client is a WebFinger client. 119 | type Client struct { 120 | // HTTP client used to perform WebFinger lookups. 121 | client *http.Client 122 | 123 | // WebFistServer is the host used for issuing WebFist queries when standard 124 | // WebFinger lookup fails. If set to the empty string, queries will not fall 125 | // back to the WebFist protocol. 126 | WebFistServer string 127 | 128 | // Allow the use of HTTP endoints for lookups. The WebFinger spec requires 129 | // all lookups be performed over HTTPS, so this should only ever be enabled 130 | // for development. 131 | AllowHTTP bool 132 | } 133 | 134 | // DefaultClient is the default Client and is used by Lookup. 135 | var DefaultClient = &Client{ 136 | client: http.DefaultClient, 137 | WebFistServer: webFistDefaultServer, 138 | } 139 | 140 | // Lookup returns the JRD for the specified identifier. 141 | // 142 | // Lookup is a wrapper around DefaultClient.Lookup. 143 | func Lookup(identifier string, rels []string) (*jrd.JRD, error) { 144 | return DefaultClient.Lookup(identifier, rels) 145 | } 146 | 147 | // NewClient returns a new WebFinger Client. If a nil http.Client is provied, 148 | // http.DefaultClient will be used. New Clients will use the default WebFist 149 | // host if WebFinger lookup fails. 150 | func NewClient(httpClient *http.Client) *Client { 151 | if httpClient == nil { 152 | httpClient = http.DefaultClient 153 | } 154 | return &Client{ 155 | client: httpClient, 156 | WebFistServer: webFistDefaultServer, 157 | } 158 | } 159 | 160 | // Lookup returns the JRD for the specified identifier. If provided, only the 161 | // specified rel values will be requested, though WebFinger servers are not 162 | // obligated to respect that request. 163 | func (c *Client) Lookup(identifier string, rels []string) (*jrd.JRD, error) { 164 | resource, err := Parse(identifier) 165 | if err != nil { 166 | return nil, err 167 | } 168 | 169 | return c.LookupResource(resource, rels) 170 | } 171 | 172 | // LookupResource returns the JRD for the specified Resource. If provided, 173 | // only the specified rel values will be requested, though WebFinger servers 174 | // are not obligated to respect that request. 175 | func (c *Client) LookupResource(resource *Resource, rels []string) (*jrd.JRD, error) { 176 | log.Printf("Looking up WebFinger data for %s", resource) 177 | 178 | resourceJRD, err := c.fetchJRD(resource.JRDURL("", rels)) 179 | if err != nil { 180 | log.Print(err) 181 | 182 | // Fallback to WebFist protocol 183 | if c.WebFistServer != "" { 184 | log.Print("Falling back to WebFist protocol") 185 | resourceJRD, err = c.webfistLookup(resource) 186 | } 187 | 188 | if err != nil { 189 | return nil, err 190 | } 191 | } 192 | 193 | return resourceJRD, nil 194 | } 195 | 196 | func (c *Client) fetchJRD(jrdURL *url.URL) (*jrd.JRD, error) { 197 | // TODO verify signature if not https 198 | // TODO extract http cache info 199 | 200 | // Get follows up to 10 redirects 201 | log.Printf("GET %s", jrdURL.String()) 202 | res, err := c.client.Get(jrdURL.String()) 203 | if err != nil { 204 | errString := strings.ToLower(err.Error()) 205 | // For some crazy reason, App Engine returns a "ssl_certificate_error" when 206 | // unable to connect to an HTTPS URL, so we check for that as well here. 207 | if (strings.Contains(errString, "connection refused") || 208 | strings.Contains(errString, "ssl_certificate_error")) && c.AllowHTTP { 209 | jrdURL.Scheme = "http" 210 | log.Printf("GET %s", jrdURL.String()) 211 | res, err = c.client.Get(jrdURL.String()) 212 | if err != nil { 213 | return nil, err 214 | } 215 | } else { 216 | return nil, err 217 | } 218 | } 219 | 220 | if !(200 <= res.StatusCode && res.StatusCode < 300) { 221 | return nil, errors.New(res.Status) 222 | } 223 | 224 | content, err := ioutil.ReadAll(res.Body) 225 | res.Body.Close() 226 | if err != nil { 227 | return nil, err 228 | } 229 | 230 | ct := strings.ToLower(res.Header.Get("content-type")) 231 | if strings.Contains(ct, "application/jrd+json") || 232 | strings.Contains(ct, "application/json") { 233 | parsed, err := jrd.ParseJRD(content) 234 | if err != nil { 235 | return nil, err 236 | } 237 | return parsed, nil 238 | } 239 | 240 | return nil, fmt.Errorf("invalid content-type: %s", ct) 241 | } 242 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package webfinger 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "net/url" 9 | "reflect" 10 | "testing" 11 | 12 | "github.com/ant0ine/go-webfinger/jrd" 13 | ) 14 | 15 | var ( 16 | // mux is the HTTP request multiplexer used with the test server. 17 | mux *http.ServeMux 18 | 19 | // server is a test HTTP server used to provide mock API responses. 20 | server *httptest.Server 21 | 22 | // testHost is the hostname and port of the local running test server. 23 | testHost string 24 | 25 | // client is the WebFinger client being tested. 26 | client *Client 27 | ) 28 | 29 | // setup a local HTTP server for testing 30 | func setup() { 31 | // test server 32 | mux = http.NewServeMux() 33 | server = httptest.NewTLSServer(mux) 34 | u, _ := url.Parse(server.URL) 35 | testHost = u.Host 36 | 37 | // for testing, use an HTTP client which doesn't check certs 38 | client = NewClient(&http.Client{ 39 | Transport: &http.Transport{ 40 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 41 | }, 42 | }) 43 | } 44 | 45 | func teardown() { 46 | server.Close() 47 | } 48 | 49 | func TestResource_Parse(t *testing.T) { 50 | // URL with host 51 | r, err := Parse("http://example.com/") 52 | if err != nil { 53 | t.Errorf("Unexpected error: %#v", err) 54 | } 55 | want := &Resource{Scheme: "http", Host: "example.com", Path: "/"} 56 | if !reflect.DeepEqual(r, want) { 57 | t.Errorf("Parsed resource: %#v, want %#v", r, want) 58 | } 59 | 60 | // email-like identifier 61 | r, err = Parse("bob@example.com") 62 | if err != nil { 63 | t.Errorf("Unexpected error: %#v", err) 64 | } 65 | want = &Resource{Scheme: "acct", Opaque: "bob@example.com"} 66 | if !reflect.DeepEqual(r, want) { 67 | t.Errorf("Parsed resource: %#v, want %#v", r, want) 68 | } 69 | } 70 | 71 | func TestResource_Parse_error(t *testing.T) { 72 | _, err := Parse("example.com") 73 | if err == nil { 74 | t.Error("Expected parse error") 75 | } 76 | 77 | _, err = Parse("%") 78 | if err == nil { 79 | t.Error("Expected parse error") 80 | } 81 | } 82 | 83 | func TestResource_WebFingerHost(t *testing.T) { 84 | // URL with host 85 | r, _ := Parse("http://example.com/") 86 | if got, want := r.WebFingerHost(), "example.com"; got != want { 87 | t.Errorf("WebFingerHost() returned: %#v, want %#v", got, want) 88 | } 89 | 90 | // email-like identifier 91 | r, _ = Parse("bob@example.com") 92 | if got, want := r.WebFingerHost(), "example.com"; got != want { 93 | t.Errorf("WebFingerHost() returned: %#v, want %#v", got, want) 94 | } 95 | 96 | // mailto URL 97 | r, _ = Parse("mailto:bob@example.com") 98 | if got, want := r.WebFingerHost(), "example.com"; got != want { 99 | t.Errorf("WebFingerHost() returned: %#v, want %#v", got, want) 100 | } 101 | 102 | // URL with no host 103 | r, _ = Parse("file:///example") 104 | if got, want := r.WebFingerHost(), ""; got != want { 105 | t.Errorf("WebFingerHost() returned: %#v, want %#v", got, want) 106 | } 107 | } 108 | 109 | func TestResource_JRDURL(t *testing.T) { 110 | r, _ := Parse("bob@example.com") 111 | got := r.JRDURL("", nil) 112 | want, _ := url.Parse("https://example.com/.well-known/webfinger?" + 113 | "resource=acct%3Abob%40example.com") 114 | if !reflect.DeepEqual(got, want) { 115 | t.Errorf("JRDURL() returned: %#v, want %#v", got, want) 116 | } 117 | 118 | r, _ = Parse("http://example.com/") 119 | got = r.JRDURL("example.net", []string{"a", "b"}) 120 | // sadly, we have to compare each URL component individually because the 121 | // order of query string values is unpredictable 122 | if want := "https"; got.Scheme != want { 123 | t.Errorf("JRDURL() returned scheme: %#v, want %#v", got.Scheme, want) 124 | } 125 | if want := "example.net"; got.Host != want { 126 | t.Errorf("JRDURL() returned host: %#v, want %#v", got.Host, want) 127 | } 128 | if want := "/.well-known/webfinger"; got.Path != want { 129 | t.Errorf("JRDURL() returned path: %#v, want %#v", got.Path, want) 130 | } 131 | if want := []string{"http://example.com/"}; reflect.DeepEqual(got.Query().Get("resource"), want) { 132 | t.Errorf("JRDURL() returned query resource: %#v, want %#v", got.Query().Get("resource"), want) 133 | } 134 | if want := []string{"a", "b"}; reflect.DeepEqual(got.Query().Get("rel"), want) { 135 | t.Errorf("JRDURL() returned query rel: %#v, want %#v", got.Query().Get("rel"), want) 136 | } 137 | } 138 | 139 | func TestResource_String(t *testing.T) { 140 | r, _ := Parse("bob@example.com") 141 | if got, want := r.String(), "acct:bob@example.com"; got != want { 142 | t.Errorf("String() returned: %#v, want %#v", got, want) 143 | } 144 | } 145 | 146 | func TestLookup(t *testing.T) { 147 | setup() 148 | defer teardown() 149 | 150 | mux.HandleFunc("/.well-known/webfinger", func(w http.ResponseWriter, r *http.Request) { 151 | resource := r.FormValue("resource") 152 | if want := "acct:bob@" + testHost; resource != want { 153 | t.Errorf("Requested resource: %v, want %v", resource, want) 154 | } 155 | w.Header().Add("content-type", "application/jrd+json") 156 | fmt.Fprint(w, `{"subject":"bob@example.com"}`) 157 | }) 158 | 159 | JRD, err := client.Lookup("bob@"+testHost, nil) 160 | if err != nil { 161 | t.Errorf("Unexpected error lookup up webfinger: %#v", err) 162 | } 163 | want := &jrd.JRD{Subject: "bob@example.com"} 164 | if !reflect.DeepEqual(JRD, want) { 165 | t.Errorf("Lookup returned %#v, want %#v", JRD, want) 166 | } 167 | } 168 | 169 | func TestLookup_parseError(t *testing.T) { 170 | // use default client here, just to make sure that gets tested 171 | _, err := Lookup("bob", nil) 172 | if err == nil { 173 | t.Error("Expected parse error") 174 | } 175 | } 176 | 177 | func TestLookup_404(t *testing.T) { 178 | setup() 179 | defer teardown() 180 | 181 | _, err := client.Lookup("bob@"+testHost, nil) 182 | if err == nil { 183 | t.Error("Expected error") 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /examples/cmdline/.gitignore: -------------------------------------------------------------------------------- 1 | webfinger 2 | -------------------------------------------------------------------------------- /examples/cmdline/Makefile: -------------------------------------------------------------------------------- 1 | PREFIX=/usr/local/ 2 | 3 | all: webfinger 4 | 5 | webfinger: main.go 6 | go build -v -o webfinger main.go 7 | 8 | install: 9 | cp webfinger $(PREFIX)bin/ 10 | -------------------------------------------------------------------------------- /examples/cmdline/main.go: -------------------------------------------------------------------------------- 1 | // TODO 2 | // * improve JRD output 3 | // * do stuff with the JRD 4 | package main 5 | 6 | import ( 7 | "encoding/json" 8 | "flag" 9 | "fmt" 10 | "github.com/ant0ine/go-webfinger" 11 | "io/ioutil" 12 | "log" 13 | "os" 14 | ) 15 | 16 | func printHelp() { 17 | fmt.Println("webfinger [-vh] ") 18 | flag.PrintDefaults() 19 | fmt.Println("example: webfinger -v bob@example.com") // same Bob as in the draft 20 | } 21 | 22 | func main() { 23 | 24 | // cmd line flags 25 | verbose := flag.Bool("v", false, "print details about the resolution") 26 | help := flag.Bool("h", false, "display this message") 27 | flag.Parse() 28 | 29 | if *help { 30 | printHelp() 31 | os.Exit(0) 32 | } 33 | 34 | if !*verbose { 35 | log.SetOutput(ioutil.Discard) 36 | } 37 | 38 | email := flag.Arg(0) 39 | 40 | if email == "" { 41 | printHelp() 42 | os.Exit(1) 43 | } 44 | 45 | log.SetFlags(0) 46 | 47 | client := webfinger.NewClient(nil) 48 | client.AllowHTTP = true 49 | 50 | jrd, err := client.Lookup(email, nil) 51 | if err != nil { 52 | fmt.Println(err) 53 | os.Exit(1) 54 | } 55 | 56 | bytes, err := json.MarshalIndent(jrd, "", " ") 57 | if err != nil { 58 | panic(err) 59 | } 60 | fmt.Printf("%s\n", bytes) 61 | 62 | os.Exit(0) 63 | } 64 | -------------------------------------------------------------------------------- /examples/simple/main.go: -------------------------------------------------------------------------------- 1 | // Minimal example, used in the README 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "github.com/ant0ine/go-webfinger" 7 | "os" 8 | ) 9 | 10 | func main() { 11 | email := os.Args[1] 12 | 13 | client := webfinger.NewClient(nil) 14 | client.AllowHTTP = true 15 | 16 | jrd, err := client.Lookup(email, nil) 17 | if err != nil { 18 | fmt.Println(err) 19 | return 20 | } 21 | 22 | fmt.Printf("JRD: %+v", jrd) 23 | } 24 | -------------------------------------------------------------------------------- /jrd/parser.go: -------------------------------------------------------------------------------- 1 | // Package jrd provides a simple JRD parser. 2 | // 3 | // Following this JRD spec: http://tools.ietf.org/html/draft-ietf-appsawg-webfinger-14#section-4.4 4 | // 5 | package jrd 6 | 7 | import ( 8 | "encoding/json" 9 | ) 10 | 11 | // JRD is a JSON Resource Descriptor, specifying properties and related links 12 | // for a resource. 13 | type JRD struct { 14 | Subject string `json:"subject,omitempty"` 15 | Aliases []string `json:"aliases,omitempty"` 16 | Properties map[string]interface{} `json:"properties,omitempty"` 17 | Links []Link `json:"links,omitempty"` 18 | } 19 | 20 | // Link is a link to a related resource. 21 | type Link struct { 22 | Rel string `json:"rel,omitempty"` 23 | Type string `json:"type,omitempty"` 24 | Href string `json:"href,omitempty"` 25 | Titles map[string]string `json:"titles,omitempty"` 26 | Properties map[string]interface{} `json:"properties,omitempty"` 27 | } 28 | 29 | // ParseJRD parses the JRD using json.Unmarshal. 30 | func ParseJRD(blob []byte) (*JRD, error) { 31 | jrd := JRD{} 32 | err := json.Unmarshal(blob, &jrd) 33 | if err != nil { 34 | return nil, err 35 | } 36 | return &jrd, nil 37 | } 38 | 39 | // GetLinkByRel returns the first *Link with the specified rel value. 40 | func (jrd *JRD) GetLinkByRel(rel string) *Link { 41 | for _, link := range jrd.Links { 42 | if link.Rel == rel { 43 | return &link 44 | } 45 | } 46 | return nil 47 | } 48 | 49 | // GetProperty Returns the property value as a string. 50 | // Per spec a property value can be null, empty string is returned in this case. 51 | func (jrd *JRD) GetProperty(uri string) string { 52 | if jrd.Properties[uri] == nil { 53 | return "" 54 | } 55 | return jrd.Properties[uri].(string) 56 | } 57 | 58 | // GetProperty Returns the property value as a string. 59 | // Per spec a property value can be null, empty string is returned in this case. 60 | func (link *Link) GetProperty(uri string) string { 61 | if link.Properties[uri] == nil { 62 | return "" 63 | } 64 | return link.Properties[uri].(string) 65 | } 66 | -------------------------------------------------------------------------------- /jrd/parser_test.go: -------------------------------------------------------------------------------- 1 | package jrd 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestParseJRD(t *testing.T) { 8 | 9 | // Adapted from spec http://tools.ietf.org/html/rfc6415#appendix-A 10 | blob := ` 11 | { 12 | "subject":"http://blog.example.com/article/id/314", 13 | 14 | "aliases":[ 15 | "http://blog.example.com/cool_new_thing", 16 | "http://blog.example.com/steve/article/7"], 17 | 18 | "properties":{ 19 | "http://blgx.example.net/ns/version":"1.3", 20 | "http://blgx.example.net/ns/ext":null 21 | }, 22 | 23 | "links":[ 24 | { 25 | "rel":"author", 26 | "type":"text/html", 27 | "href":"http://blog.example.com/author/steve", 28 | "titles":{ 29 | "default":"About the Author", 30 | "en-us":"Author Information" 31 | }, 32 | "properties":{ 33 | "http://example.com/role":"editor" 34 | } 35 | }, 36 | { 37 | "rel":"author", 38 | "href":"http://example.com/author/john", 39 | "titles":{ 40 | "default":"The other author" 41 | } 42 | }, 43 | { 44 | "rel":"copyright" 45 | } 46 | ] 47 | } 48 | ` 49 | obj, err := ParseJRD([]byte(blob)) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | if got, want := obj.Subject, "http://blog.example.com/article/id/314"; got != want { 54 | t.Errorf("JRD.Subject is %q, want %q", got, want) 55 | } 56 | if got, want := obj.GetProperty("http://blgx.example.net/ns/version"), "1.3"; got != want { 57 | t.Errorf("obj.GetProperty('http://blgx.example.net/ns/version') returned %q, want %q", got, want) 58 | } 59 | if got, want := obj.GetProperty("http://blgx.example.net/ns/ext"), ""; got != want { 60 | t.Errorf("obj.GetProperty('http://blgx.example.net/ns/ext') returned %q, want %q", got, want) 61 | } 62 | if obj.GetLinkByRel("copyright") == nil { 63 | t.Error("obj.GetLinkByRel('copyright') returned nil, want non-nil value") 64 | } 65 | if got, want := obj.GetLinkByRel("author").Titles["default"], "About the Author"; got != want { 66 | t.Errorf("obj.GetLinkByRel('author').Titles['default'] returned %q, want %q", got, want) 67 | } 68 | if got, want := obj.GetLinkByRel("author").GetProperty("http://example.com/role"), "editor"; got != want { 69 | t.Errorf("obj.GetLinkByRel('author').GetProperty('http://example.com/role') returned %q, want %q", got, want) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /webfist.go: -------------------------------------------------------------------------------- 1 | package webfinger 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/url" 7 | 8 | "github.com/ant0ine/go-webfinger/jrd" 9 | ) 10 | 11 | const ( 12 | webFistDefaultServer = "webfist.org" 13 | webFistRel = "http://webfist.org/spec/rel" 14 | ) 15 | 16 | func (c *Client) webfistLookup(resource *Resource) (*jrd.JRD, error) { 17 | jrdURL := resource.JRDURL(c.WebFistServer, nil) 18 | webfistJRD, err := c.fetchJRD(jrdURL) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | link := webfistJRD.GetLinkByRel(webFistRel) 24 | if link == nil { 25 | return nil, fmt.Errorf("No WebFist link") 26 | } 27 | 28 | u, err := url.Parse(link.Href) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | log.Printf("Found WebFist link: %s", u) 34 | return c.fetchJRD(u) 35 | } 36 | -------------------------------------------------------------------------------- /webfist_test.go: -------------------------------------------------------------------------------- 1 | package webfinger 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "net/url" 8 | "reflect" 9 | "testing" 10 | 11 | "github.com/ant0ine/go-webfinger/jrd" 12 | ) 13 | 14 | var ( 15 | wfMux *http.ServeMux 16 | wfServer *httptest.Server 17 | wfTestHost string 18 | ) 19 | 20 | func webFistSetup() { 21 | setup() 22 | 23 | wfMux = http.NewServeMux() 24 | wfServer = httptest.NewTLSServer(wfMux) 25 | u, _ := url.Parse(wfServer.URL) 26 | wfTestHost = u.Host 27 | 28 | client.WebFistServer = wfTestHost 29 | } 30 | 31 | func webFistTearDown() { 32 | teardown() 33 | wfServer.Close() 34 | } 35 | 36 | func TestWebFistLookup(t *testing.T) { 37 | webFistSetup() 38 | defer webFistTearDown() 39 | 40 | mux.HandleFunc("/webfinger.json", func(w http.ResponseWriter, r *http.Request) { 41 | w.Header().Add("content-type", "application/jrd+json") 42 | fmt.Fprint(w, `{"subject":"bob@example.com"}`) 43 | }) 44 | 45 | // simulate WebFist protocol 46 | wfMux.HandleFunc("/.well-known/webfinger", func(w http.ResponseWriter, r *http.Request) { 47 | resource := r.FormValue("resource") 48 | if want := "acct:bob@" + testHost; resource != want { 49 | t.Errorf("Requested resource: %v, want %v", resource, want) 50 | } 51 | w.Header().Add("content-type", "application/jrd+json") 52 | fmt.Fprint(w, `{ 53 | "links": [{ 54 | "rel": "http://webfist.org/spec/rel", 55 | "href": "`+server.URL+`/webfinger.json" 56 | }] 57 | }`) 58 | }) 59 | 60 | JRD, err := client.Lookup("bob@"+testHost, nil) 61 | if err != nil { 62 | t.Errorf("Unexpected error lookup up webfinger: %#v", err) 63 | } 64 | want := &jrd.JRD{Subject: "bob@example.com"} 65 | if !reflect.DeepEqual(JRD, want) { 66 | t.Errorf("Lookup returned %#v, want %#v", JRD, want) 67 | } 68 | } 69 | 70 | func TestWebFistLookup_noLink(t *testing.T) { 71 | webFistSetup() 72 | defer webFistTearDown() 73 | 74 | wfMux.HandleFunc("/.well-known/webfinger", func(w http.ResponseWriter, r *http.Request) { 75 | w.Header().Add("content-type", "application/jrd+json") 76 | fmt.Fprint(w, `{}`) 77 | }) 78 | 79 | _, err := client.Lookup("bob@"+testHost, nil) 80 | if err == nil { 81 | t.Errorf("Expected webfist error.") 82 | } 83 | } 84 | 85 | func TestWebFistLookup_invalidLink(t *testing.T) { 86 | webFistSetup() 87 | defer webFistTearDown() 88 | 89 | wfMux.HandleFunc("/.well-known/webfinger", func(w http.ResponseWriter, r *http.Request) { 90 | w.Header().Add("content-type", "application/jrd+json") 91 | fmt.Fprint(w, `{ 92 | "links": [{ 93 | "rel": "http://webfist.org/spec/rel", 94 | "href": "#" 95 | }] 96 | }`) 97 | }) 98 | 99 | _, err := client.Lookup("bob@"+testHost, nil) 100 | if err == nil { 101 | t.Errorf("Expected webfist error.") 102 | } 103 | } 104 | --------------------------------------------------------------------------------