webfinger.ml 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. (*
  2. * _ _ ____ _
  3. * _| || |_/ ___| ___ _ __ _ __ ___ | |
  4. * |_ .. _\___ \ / _ \ '_ \| '_ \ / _ \| |
  5. * |_ _|___) | __/ |_) | |_) | (_) |_|
  6. * |_||_| |____/ \___| .__/| .__/ \___/(_)
  7. * |_| |_|
  8. *
  9. * Personal Social Web.
  10. *
  11. * Copyright (C) The #Seppo contributors. All rights reserved.
  12. *
  13. * This program is free software: you can redistribute it and/or modify
  14. * it under the terms of the GNU General Public License as published by
  15. * the Free Software Foundation, either version 3 of the License, or
  16. * (at your option) any later version.
  17. *
  18. * This program is distributed in the hope that it will be useful,
  19. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  20. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  21. * GNU General Public License for more details.
  22. *
  23. * You should have received a copy of the GNU General Public License
  24. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  25. *)
  26. let to_result none = Option.to_result ~none
  27. let ( >>= ) = Result.bind
  28. let ( let* ) = Result.bind
  29. let chain a b =
  30. let f a = Ok (a, b) in
  31. Result.bind a f
  32. let writev oc j =
  33. Ezjsonm.value_to_channel ~minify:false oc j;
  34. Ok ""
  35. (* https://datatracker.ietf.org/doc/html/rfc7565 *)
  36. let scheme = "acct"
  37. let rx = {|^\(\(|} ^ scheme ^ {|:\)\|@\)?\([^@: ]+\)@\([^ ]+\)|} |> Str.regexp
  38. type rfc7565 = Rfc7565 of Uri.t
  39. let of_string s =
  40. let u = if Str.string_match rx s 0
  41. then
  42. let userinfo = s |> Str.matched_group 3
  43. and host = s |> Str.matched_group 4
  44. in Uri.make ~scheme ~userinfo ~host ()
  45. else s |> Uri.of_string in
  46. Rfc7565 u
  47. let to_string (Rfc7565 u) =
  48. assert (scheme = "acct");
  49. let s = u |> Uri.to_string in
  50. match s |> St.after ~prefix:"acct://" with
  51. | Some s -> "acct:" ^ s
  52. | None -> s
  53. let dir = ".well-known/webfinger"
  54. let well_known_uri (Rfc7565 u' as u) =
  55. let path = "/" ^ dir in
  56. let u = u |> to_string in
  57. let* host = u' |> Uri.host |> Option.to_result ~none:("host mandatory: " ^ u) in
  58. Ok (Uri.make
  59. ~scheme:"https"
  60. ~host
  61. ~path
  62. ~query:["resource", [u]]
  63. ())
  64. let apa = "activitypub/actor.jsa" (* redeclare Ap.proj to avoid dependency cycle *)
  65. (* https://tools.ietf.org/html/rfc7033
  66. *)
  67. module Client = struct
  68. type l = Localpart of string
  69. type h = Domainpart of string
  70. type t = (l * h)
  71. let rfc7565_to_string = to_string
  72. let rfc7565_of_string = of_string
  73. let[@deprecated] from_uri (Rfc7565 u) : (t, string) result =
  74. match (u |> Uri.scheme), (u |> Uri.user), (u |> Uri.host), (u |> Uri.port), (u |> Uri.path), (u |> Uri.query), (u |> Uri.fragment) with
  75. | Some "acct", Some usr, Some hos, None, "", [], None ->
  76. Ok (Localpart usr, Domainpart hos)
  77. | _ ->
  78. Error ("uri must be user and host, e.g. acct://alice@example.com but was\n" ^ (u |> Uri.to_string))
  79. let[@deprecated] from_string s : (t, string) result =
  80. s
  81. |> rfc7565_of_string
  82. |> from_uri
  83. let[@deprecated] to_rfc7565 (Localpart userinfo, Domainpart host) =
  84. Rfc7565 (Uri.make ~scheme ~userinfo ~host ())
  85. let[@deprecated] to_string me =
  86. me |> to_rfc7565 |> rfc7565_to_string
  87. let to_short h =
  88. let Rfc7565 u = h |> to_rfc7565 in
  89. let s = u |> Uri.to_string in
  90. let l = s |> String.length in
  91. String.sub s 7 (l-7)
  92. let http_get
  93. ?(key : Http.t_sign_k option = None)
  94. (w : Uri.t) =
  95. let mape (_ : Ezjsonm.value Decoders__Error.t) =
  96. Logr.err (fun m -> m "%s: webfinger decode failed %a" E.e1027
  97. Uri.pp w);
  98. E.e1027 ^ ": webfinger decode failed" in
  99. let deco j = j
  100. |> As2_vocab.Decode.Webfinger.query_result
  101. |> Result.map_error mape in
  102. let headers = [Http.H.acc_app_jrd] |> Cohttp.Header.of_list in
  103. let%lwt p = w |> Http.get_jsonv ~key ~headers Result.ok in
  104. p
  105. >>= deco
  106. |> Lwt.return
  107. end
  108. let make (Auth.Uid uid, base) : As2_vocab.Types.Webfinger.query_result =
  109. let host = base |> Uri.host |> Option.value ~default:"-" in
  110. let subject = Printf.sprintf "%s:%s@%s" scheme uid host in
  111. let tmpl = Format.asprintf "%aseppo.cgi/activitypub/actor.xml?id={uri}" Uri.pp base in
  112. let open As2_vocab.Types.Webfinger in
  113. let path = apa in
  114. let links = [
  115. Self (`ActivityJsonLd, Uri.make ~path ());
  116. ProfilePage (`Html, Uri.make ~path:"." ());
  117. Alternate (`Atom, Rfc4287.defa);
  118. OStatusSubscribe tmpl;
  119. ] in
  120. {subject;aliases=[];links}
  121. let jsonm (uid, base) : (Ezjsonm.value,'a) result =
  122. (uid, base)
  123. |> make
  124. |> As2_vocab.Encode.Webfinger.query_result ~base
  125. |> Result.ok
  126. let target = dir ^ "/index.jrd"
  127. let rule : Make.t =
  128. { target;
  129. prerequisites = [ apa ];
  130. fresh = Make.Outdated;
  131. command = fun _ _ru _all ->
  132. File.out_channel' (fun oc ->
  133. Cfg.Base.(fn |> from_file)
  134. >>= chain Auth.(fn |> uid_from_file)
  135. >>= jsonm
  136. >>= writev oc)
  137. }
  138. let rulez = rule :: [] (* :: Ap.Person.rulez *)
  139. module Server = struct
  140. (* Create a local .well-known/webfinger and link here from the global one (in webroot). *)
  141. let target = dir ^ "/.htaccess"
  142. let rule : Make.t = {
  143. target;
  144. prerequisites = [ rule.target ];
  145. fresh = Make.Outdated;
  146. command = fun _pre _ _ ->
  147. File.out_channel' (fun oc ->
  148. let* (Auth.Uid uid),_ = Auth.(from_file fn) in
  149. let* base = Cfg.Base.(from_file fn) in
  150. let pat = base |> Uri.path in
  151. Printf.fprintf oc "# https://%s/S1002\n\
  152. # automatically linked or manually appended to <webroot>/%s\n\
  153. # created by ../../seppo.cgi\n\
  154. RewriteEngine On\n\
  155. RewriteCond %%{QUERY_STRING} (?i)^(.+?&)?resource=%s:%s@.+$\n\
  156. RewriteRule ^$ %s%s/index.jrd [qsdiscard,last,redirect=seeother]\n"
  157. St.seppo_s target scheme (uid |> Str.quote) pat dir;
  158. if not (pat |> String.equal "/")
  159. then (
  160. assert (pat |> St.starts_with ~prefix:"/");
  161. assert (pat |> St.ends_with ~suffix:"/");
  162. assert (target |> St.updir |> String.equal "../../");
  163. let prefi = pat |> St.updir in
  164. let dst = prefi ^ target in
  165. let _ = dst |> Filename.dirname |> File.mkdir_p File.pDir in
  166. if Unix.(try S_LNK == (lstat dst).st_kind
  167. with | _ -> false)
  168. then (
  169. Logr.debug (fun m -> m "%s.%s remove symlink %s" "Webfinger.Server" "rule" dst);
  170. try Unix.unlink dst
  171. with | e -> Logr.debug (fun m -> m "%s.%s counldn't remove %s: %s" "Make" "make" dst (Printexc.to_string e)) )
  172. else
  173. Logr.warn (fun m -> m "%s.%s %s %s isn't a symlink, so I don't interfere with it. Do that manually." "Webfinger.Server" "rule" E.e1031 dst);
  174. let src = "../.." ^ pat ^ target in
  175. Logr.debug (fun m -> m "%s.%s ln -s %s %s" "Webfinger.Server" "rule" src dst);
  176. try Unix.symlink src dst
  177. with | e -> Logr.err (fun m -> m "%s.%s 3 %s" "Make" "make" (Printexc.to_string e))
  178. );
  179. Ok "")
  180. }
  181. let make = Make.make [rule]
  182. end