A golang webfinger server implementation
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

289 lines
8.2 KiB

  1. package webfinger
  2. import (
  3. "bytes"
  4. "net/http"
  5. "net/url"
  6. "sort"
  7. "testing"
  8. "reflect"
  9. "crypto/tls"
  10. "strings"
  11. "github.com/pkg/errors"
  12. )
  13. type dummyUserResolver struct {
  14. }
  15. func (d *dummyUserResolver) FindUser(username string, hostname, requestHost string, rel []Rel) (*Resource, error) {
  16. if username == "hello" {
  17. if len(rel) == 2 && rel[0] == "x" && rel[1] == "y" {
  18. return &Resource{
  19. Links: []Link{
  20. Link{
  21. HRef: string(rel[0]),
  22. Rel: string(rel[0]),
  23. },
  24. Link{
  25. HRef: string(rel[1]),
  26. Rel: string(rel[1]),
  27. },
  28. },
  29. }, nil
  30. }
  31. return &Resource{
  32. Links: []Link{
  33. Link{
  34. HRef: string("x"),
  35. Rel: string("x"),
  36. },
  37. Link{
  38. HRef: string("y"),
  39. Rel: string("y"),
  40. },
  41. Link{
  42. HRef: string("z"),
  43. Rel: string("z"),
  44. },
  45. },
  46. }, nil
  47. }
  48. return nil, errors.New("User not found")
  49. }
  50. func (d *dummyUserResolver) DummyUser(username string, hostname string, rel []Rel) (*Resource, error) {
  51. return nil, errors.New("User not found")
  52. }
  53. func (d *dummyUserResolver) IsNotFoundError(err error) bool {
  54. if err == nil {
  55. return false
  56. }
  57. if err.Error() == "User not found" {
  58. return true
  59. }
  60. if errors.Cause(err).Error() == "User not found" {
  61. return true
  62. }
  63. return false
  64. }
  65. type dummyResponseWriter struct {
  66. bytes.Buffer
  67. code int
  68. headers http.Header
  69. }
  70. func (d *dummyResponseWriter) Header() http.Header {
  71. if d.headers == nil {
  72. d.headers = make(http.Header)
  73. }
  74. return d.headers
  75. }
  76. func (d *dummyResponseWriter) WriteHeader(i int) {
  77. d.code = i
  78. }
  79. type serveHTTPTest struct {
  80. Description string
  81. Input *http.Request
  82. OutputCode int
  83. OutputHeaders http.Header
  84. OutputBody string
  85. }
  86. type kv struct {
  87. key string
  88. val string
  89. }
  90. func buildRequest(method string, path string, query string, kvx ...kv) *http.Request {
  91. r := &http.Request{}
  92. r.Host = "http://localhost"
  93. r.Header = make(http.Header)
  94. r.URL = &url.URL{
  95. Path: path,
  96. RawQuery: query,
  97. Host: "localhost",
  98. Scheme: "http",
  99. }
  100. for _, k := range kvx {
  101. r.Header.Add(k.key, k.val)
  102. }
  103. r.Method = method
  104. return r
  105. }
  106. func buildRequestTLS(method string, path string, query string, kvx ...kv) *http.Request {
  107. r := &http.Request{}
  108. r.Host = "https://localhost"
  109. r.Header = make(http.Header)
  110. r.URL = &url.URL{
  111. Path: path,
  112. RawQuery: query,
  113. Host: "localhost",
  114. Scheme: "https",
  115. }
  116. r.TLS = &tls.ConnectionState{} // marks the request as TLS
  117. for _, k := range kvx {
  118. r.Header.Add(k.key, k.val)
  119. }
  120. r.Method = method
  121. return r
  122. }
  123. var defaultHeaders = http.Header(map[string][]string{
  124. "Cache-Control": []string{"no-cache"},
  125. "Pragma": []string{"no-cache"},
  126. "Content-Type": []string{"application/jrd+json"},
  127. })
  128. func plusHeader(h http.Header, kvx ...kv) http.Header {
  129. var h2 = make(http.Header)
  130. for k, vx := range h {
  131. for _, v := range vx {
  132. h2.Add(k, v)
  133. }
  134. }
  135. for _, k := range kvx {
  136. h2.Add(k.key, k.val)
  137. }
  138. return h2
  139. }
  140. func compareHeaders(h1 http.Header, h2 http.Header) bool {
  141. if len(h1) != len(h2) {
  142. return false
  143. }
  144. keys := []string{}
  145. for k := range h1 {
  146. keys = append(keys, k)
  147. }
  148. sort.Strings(keys)
  149. for _, k := range keys {
  150. if !reflect.DeepEqual(h1[k], h2[k]) {
  151. return false
  152. }
  153. }
  154. return true
  155. }
  156. func TestServiceServeHTTP(t *testing.T) {
  157. var tests = []serveHTTPTest{
  158. {"GET root URL should return 404 not found with no headers",
  159. buildRequestTLS("GET", "/", ""), http.StatusNotFound, make(http.Header), ""},
  160. {"POST root URL should return 404 not found with no headers",
  161. buildRequestTLS("POST", "/", ""), http.StatusNotFound, make(http.Header), ""},
  162. {"GET /.well-known should return 404 not found with no headers",
  163. buildRequestTLS("GET", "/.well-known", ""), http.StatusNotFound, make(http.Header), ""},
  164. {"POST /.well-known should return 404 not found with no headers",
  165. buildRequestTLS("POST", "/.well-known", ""), http.StatusNotFound, make(http.Header), ""},
  166. /*
  167. 4.2. Performing a WebFinger Query
  168. */
  169. /*
  170. A WebFinger client issues a query using the GET method to the well-
  171. known [3] resource identified by the URI whose path component is
  172. "/.well-known/webfinger" and whose query component MUST include the
  173. "resource" parameter exactly once and set to the value of the URI for
  174. which information is being sought.
  175. */
  176. {"GET Webfinger resource should return valid response with default headers",
  177. buildRequestTLS("GET", WebFingerPath, "resource=acct:hello@domain"), http.StatusOK, defaultHeaders,
  178. `{"links":[{"href":"x","ref":"x"},{"href":"y","ref":"y"},{"href":"z","ref":"z"}]}`},
  179. {"POST Webfinger URL should fail with MethodNotAllowed",
  180. buildRequestTLS("POST", WebFingerPath, ""), http.StatusMethodNotAllowed, defaultHeaders, ""},
  181. {"GET multiple resources should fail with BadRequest",
  182. buildRequestTLS("GET", WebFingerPath, "resource=acct:hello@domain&resource=acct:hello2@domain"), http.StatusBadRequest, defaultHeaders, ""},
  183. /*
  184. The "rel" parameter MAY be included multiple times in order to
  185. request multiple link relation types.
  186. */
  187. {"GET Webfinger resource with rel should filter results",
  188. buildRequestTLS("GET", WebFingerPath, "resource=acct:hello@domain&rel=x&rel=y"), http.StatusOK, defaultHeaders,
  189. `{"links":[{"href":"x","ref":"x"},{"href":"y","ref":"y"}]}`},
  190. /*
  191. A WebFinger resource MAY redirect the client; if it does, the
  192. redirection MUST only be to an "https" URI and the client MUST
  193. perform certificate validation again when redirected.
  194. */
  195. {"GET non-TLS should redirect to TLS",
  196. buildRequest("GET", WebFingerPath, "resource=acct:hello@domain"), http.StatusSeeOther, plusHeader(
  197. defaultHeaders, kv{"Location", "https://localhost/.well-known/webfinger?resource=acct:hello@domain"}), ""},
  198. /*
  199. A WebFinger resource MUST return a JRD as the representation for the
  200. resource if the client requests no other supported format explicitly
  201. via the HTTP "Accept" header. The client MAY include the "Accept"
  202. header to indicate a desired representation; representations other
  203. than JRD might be defined in future specifications. The WebFinger
  204. resource MUST silently ignore any requested representations that it
  205. does not understand or support. The media type used for the JSON
  206. Resource Descriptor (JRD) is "application/jrd+json" (see Section
  207. 10.2).
  208. */
  209. {"GET with Accept should return as normal",
  210. buildRequestTLS("GET", WebFingerPath, "resource=acct:hello@domain", kv{"Accept", "application/json"}), http.StatusOK, defaultHeaders,
  211. `{"links":[{"href":"x","ref":"x"},{"href":"y","ref":"y"},{"href":"z","ref":"z"}]}`},
  212. /*
  213. If the "resource" parameter is a value for which the server has no
  214. information, the server MUST indicate that it was unable to match the
  215. request as per Section 10.4.5 of RFC 2616.
  216. */
  217. {"GET with a missing user should return 404",
  218. buildRequestTLS("GET", WebFingerPath, "resource=acct:missinguser@domain"), http.StatusNotFound, defaultHeaders, ""},
  219. /*
  220. If the "resource" parameter is absent or malformed, the WebFinger
  221. resource MUST indicate that the request is bad as per Section 10.4.1
  222. of RFC 2616 [2]. (400 bad request)
  223. */
  224. {"GET with no resource should fail with BadRequest",
  225. buildRequestTLS("GET", WebFingerPath, ""), http.StatusBadRequest, defaultHeaders, ""},
  226. {"GET with malformed resource URI should fail with BadRequest",
  227. buildRequestTLS("GET", WebFingerPath, "resource=hello-world"), http.StatusBadRequest, defaultHeaders, ""},
  228. {"GET with http resource URI should fail with BadRequest",
  229. buildRequestTLS("GET", WebFingerPath, "resource=http://hello-world"), http.StatusBadRequest, defaultHeaders, ""},
  230. }
  231. svc := Default(&dummyUserResolver{})
  232. for _, tx := range tests {
  233. w := &dummyResponseWriter{code: 200}
  234. svc.ServeHTTP(w, tx.Input)
  235. // code should be 404
  236. // headers should be empty
  237. body := strings.TrimSpace(string(w.Buffer.Bytes()))
  238. failed := false
  239. failed = failed || w.code != tx.OutputCode
  240. failed = failed || !compareHeaders(tx.OutputHeaders, w.headers)
  241. failed = failed || body != tx.OutputBody
  242. if failed {
  243. t.Errorf("%s\nHTTP '%v' '%v' => \n\t'%v'\n\t'%v'\n\t'%v';\nexpected \n\t'%v \n\t'%v' \n\t'%v'",
  244. tx.Description,
  245. tx.Input.Method, tx.Input.URL,
  246. w.code, w.headers, body,
  247. tx.OutputCode, tx.OutputHeaders, tx.OutputBody)
  248. }
  249. }
  250. }