···5656 in
5757 (access, refresh)
58585959+let generate_service_jwt ~did ~service_did ~lxm ~signing_key =
6060+ let now_s = int_of_float (Unix.gettimeofday ()) in
6161+ let exp = now_s + (60 * 5) in
6262+ match
6363+ Jwto.encode Jwto.HS256 signing_key
6464+ [ ("iss", did)
6565+ ; ("aud", service_did)
6666+ ; ("lxm", lxm)
6767+ ; ("exp", Int.to_string exp) ]
6868+ with
6969+ | Ok token ->
7070+ token
7171+ | Error err ->
7272+ failwith err
7373+5974let verify_bearer_jwt t token expected_scope =
6075 match Jwto.decode_and_verify Env.jwt_secret token with
6176 | Error err ->
+238-20
pegasus/lib/id_resolver.ml
···1212 let uri =
1313 Uri.of_string ("https://" ^ handle ^ "/.well-known/atproto-did")
1414 in
1515- let%lwt _, body = Client.get uri in
1616- let%lwt did = Body.to_string body in
1717- Lwt.return_ok did
1515+ let%lwt {status; _}, body = Client.get uri in
1616+ match status with
1717+ | `OK ->
1818+ let%lwt did = Body.to_string body in
1919+ Lwt.return_ok did
2020+ | _ ->
2121+ Lwt.return_error "failed to resolve"
1822 with exn -> Lwt.return_error (Printexc.to_string exn)
19232024 let resolve_dns handle =
···4044 Lwt.return_error e
4145 with exn -> Lwt.return_error (Printexc.to_string exn)
42464343- let resolve handle =
4444- (* run well-known and dns in parallel, error if they return different values, if only one returns just return that value *)
4545- match%lwt Lwt.all [resolve_well_known handle; resolve_dns handle] with
4646- | [Ok did1; Ok did2] when did1 = did2 ->
4747- Lwt.return_ok did1
4848- | [Ok _; Ok _] ->
4949- Lwt.return_error "conflicting dids"
5050- | [Ok did1; _] ->
5151- Lwt.return_ok did1
5252- | [_; Ok did2] ->
5353- Lwt.return_ok did2
5454- | [Error e1; Error e2] ->
5555- Lwt.return_error
5656- (Printf.sprintf
5757- "well-known resolution error: %s\ndns resolution error: %s" e1 e2 )
5858- | _ ->
5959- Lwt.return_error "unexpected error"
4747+ let cache = Ttl_cache.String_cache.create (3 * 60 * 60 * 1000) ()
4848+4949+ let resolve ?(skip_cache = false) handle =
5050+ match Ttl_cache.String_cache.get cache handle with
5151+ | Some from_cache when skip_cache = false ->
5252+ Lwt.return_ok from_cache
5353+ | _ -> (
5454+ (* run well-known and dns in parallel, error if they return different values, if only one returns just return that value *)
5555+ let%lwt result =
5656+ match%lwt Lwt.all [resolve_well_known handle; resolve_dns handle] with
5757+ | [Ok did1; Ok did2] when did1 = did2 ->
5858+ Lwt.return_ok did1
5959+ | [Ok _; Ok _] ->
6060+ Lwt.return_error "conflicting dids"
6161+ | [Ok did1; _] ->
6262+ Lwt.return_ok did1
6363+ | [_; Ok did2] ->
6464+ Lwt.return_ok did2
6565+ | [Error e1; Error e2] ->
6666+ Lwt.return_error
6767+ (Printf.sprintf
6868+ "well-known resolution error: %s\ndns resolution error: %s"
6969+ e1 e2 )
7070+ | _ ->
7171+ Lwt.return_error "unexpected error"
7272+ in
7373+ match result with
7474+ | Ok did ->
7575+ Ttl_cache.String_cache.set cache handle did ;
7676+ Lwt.return_ok did
7777+ | Error e ->
7878+ Lwt.return_error e )
7979+end
8080+8181+module Did = struct
8282+ open struct
8383+ type string_or_strings = [`String of string | `Strings of string list]
8484+8585+ let string_or_strings_to_yojson = function
8686+ | `String c ->
8787+ `String c
8888+ | `Strings cs ->
8989+ `List (List.map (fun c -> `String c) cs)
9090+9191+ let string_or_strings_of_yojson = function
9292+ | `String c ->
9393+ Ok (`Strings [c])
9494+ | `List cs ->
9595+ Ok (`Strings (Yojson.Safe.Util.filter_string cs))
9696+ | _ ->
9797+ Error "invalid field value"
9898+9999+ type string_or_string_map =
100100+ [`String of string | `StringMap of (string * string) list]
101101+102102+ let string_or_string_map_to_yojson = function
103103+ | `String c ->
104104+ `String c
105105+ | `StringMap m ->
106106+ `Assoc (List.map (fun (k, v) -> (k, `String v)) m)
107107+108108+ let string_or_string_map_of_yojson = function
109109+ | `String c ->
110110+ Ok (`StringMap [(c, "")])
111111+ | `Assoc m ->
112112+ Ok
113113+ (`StringMap
114114+ (List.map (fun (k, v) -> (k, Yojson.Safe.Util.to_string v)) m) )
115115+ | _ ->
116116+ Error "invalid field value"
117117+118118+ type string_or_string_map_or_either_list =
119119+ [ `String of string
120120+ | `StringMap of (string * string) list
121121+ | `List of string_or_string_map list ]
122122+123123+ let string_or_string_map_or_either_list_to_yojson = function
124124+ | `String c ->
125125+ `String c
126126+ | `StringMap m ->
127127+ `Assoc (List.map (fun (k, v) -> (k, `String v)) m)
128128+ | `List l ->
129129+ `List (List.map string_or_string_map_to_yojson l)
130130+131131+ let string_or_string_map_or_either_list_of_yojson = function
132132+ | `String c ->
133133+ Ok (`StringMap [(c, "")])
134134+ | `Assoc m ->
135135+ Ok
136136+ (`StringMap
137137+ (List.map (fun (k, v) -> (k, Yojson.Safe.Util.to_string v)) m) )
138138+ | `List l ->
139139+ Ok
140140+ (`List
141141+ ( List.map string_or_string_map_of_yojson l
142142+ |> List.filter_map (function Ok x -> Some x | Error _ -> None) )
143143+ )
144144+ | _ ->
145145+ Error "invalid field value"
146146+ end
147147+148148+ module Document = struct
149149+ type service =
150150+ { id: string
151151+ ; type': string_or_strings [@key "type"]
152152+ ; service_endpoint: string_or_string_map_or_either_list
153153+ [@key "serviceEndpoint"] }
154154+ [@@deriving yojson {strict= false}]
155155+156156+ type verification_method =
157157+ { id: string
158158+ ; type': string [@key "type"]
159159+ ; controller: string
160160+ ; public_key_multibase: string option [@key "publicKeyMultibase"] }
161161+ [@@deriving yojson {strict= false}]
162162+163163+ type string_or_verification_method =
164164+ [`String of string | `VerificationMethod of verification_method]
165165+166166+ let string_or_verification_method_to_yojson = function
167167+ | `String s ->
168168+ `String s
169169+ | `VerificationMethod vm ->
170170+ verification_method_to_yojson vm
171171+172172+ let string_or_verification_method_of_yojson = function
173173+ | `String s ->
174174+ Ok (`String s)
175175+ | `Assoc m ->
176176+ verification_method_of_yojson (`Assoc m)
177177+ |> Result.map (fun x -> `VerificationMethod x)
178178+ | _ ->
179179+ Error "invalid field value"
180180+181181+ type t =
182182+ { context: string list [@key "@context"]
183183+ ; id: string
184184+ ; controller: string_or_strings option
185185+ ; also_known_as: string list option [@key "alsoKnownAs"]
186186+ ; verification_method: verification_method list option
187187+ [@key "verificationMethod"]
188188+ ; authentication: string_or_verification_method list option
189189+ ; service: service list option }
190190+ [@@deriving yojson {strict= false}]
191191+192192+ let get_service_endpoint s =
193193+ match s.service_endpoint with
194194+ | `String e ->
195195+ e
196196+ | `List l -> (
197197+ match List.hd l with `String e -> e | `StringMap m -> List.hd m |> snd )
198198+ | `StringMap m ->
199199+ List.hd m |> snd
200200+201201+ let get_service t fragment =
202202+ match t.service with
203203+ | None ->
204204+ None
205205+ | Some services ->
206206+ List.find_map
207207+ (fun (s : service) ->
208208+ if s.id = fragment then Some (get_service_endpoint s) else None )
209209+ services
210210+ end
211211+212212+ type document = Document.t
213213+214214+ let cache = Ttl_cache.String_cache.create (12 * 60 * 60 * 1000) ()
215215+216216+ let resolve_plc did =
217217+ if not (String.starts_with ~prefix:"did:plc:" did) then
218218+ Lwt.return_error "invalid did method"
219219+ else
220220+ try%lwt
221221+ let uri =
222222+ Uri.make ~scheme:"https" ~host:"plc.directory"
223223+ ~path:(Uri.pct_encode did) ()
224224+ in
225225+ let%lwt {status; _}, body =
226226+ Client.get uri
227227+ ~headers:(Cohttp.Header.of_list [("Accept", "application/json")])
228228+ in
229229+ match status with
230230+ | `OK ->
231231+ let%lwt body = Body.to_string body in
232232+ body |> Yojson.Safe.from_string |> Document.of_yojson |> Lwt.return
233233+ | _ ->
234234+ Lwt.return_error "failed to resolve"
235235+ with e -> Lwt.return_error (Printexc.to_string e)
236236+237237+ let resolve_web did =
238238+ if not (String.starts_with ~prefix:"did:web:" did) then
239239+ Lwt.return_error "invalid did method"
240240+ else
241241+ try%lwt
242242+ let uri =
243243+ Uri.make ~scheme:"https" ~host:(Str.string_after did 8)
244244+ ~path:"/.well-known/did.json" ()
245245+ in
246246+ let%lwt {status; _}, body =
247247+ Client.get uri
248248+ ~headers:(Cohttp.Header.of_list [("Accept", "application/json")])
249249+ in
250250+ match status with
251251+ | `OK ->
252252+ let%lwt body = Body.to_string body in
253253+ body |> Yojson.Safe.from_string |> Document.of_yojson |> Lwt.return
254254+ | _ ->
255255+ Lwt.return_error "failed to resolve"
256256+ with e -> Lwt.return_error (Printexc.to_string e)
257257+258258+ let resolve ?(skip_cache = false) did =
259259+ match Ttl_cache.String_cache.get cache did with
260260+ | Some from_cache when skip_cache = false ->
261261+ Lwt.return_ok from_cache
262262+ | _ -> (
263263+ let%lwt result =
264264+ match did with
265265+ | did when String.starts_with ~prefix:"did:plc:" did ->
266266+ resolve_plc did
267267+ | did when String.starts_with ~prefix:"did:web:" did ->
268268+ resolve_web did
269269+ | _ ->
270270+ Lwt.return_error "invalid did method"
271271+ in
272272+ match result with
273273+ | Ok doc ->
274274+ Ttl_cache.String_cache.set cache did doc ;
275275+ Lwt.return_ok doc
276276+ | Error err ->
277277+ Lwt.return_error err )
60278end
+121
pegasus/lib/ttl_cache.ml
···11+module Make (K : Hashtbl.HashedType) = struct
22+ module H = Hashtbl.Make (K)
33+44+ type time_ms = int
55+66+ type 'a entry = {value: 'a; mutable expires_at: time_ms}
77+88+ type 'a t =
99+ {table: 'a entry H.t; mutable capacity: int option; default_ttl: time_ms}
1010+1111+ let default_initial_capacity = 16
1212+1313+ let[@inline] _now_ms () : time_ms = Util.now_ms ()
1414+1515+ let create ?capacity ?(initial_capacity = default_initial_capacity)
1616+ default_ttl () : 'a t =
1717+ {table= H.create initial_capacity; capacity; default_ttl}
1818+1919+ let clear (t : 'a t) : unit = H.clear t.table
2020+2121+ let remove (t : 'a t) (k : K.t) : unit = H.remove t.table k
2222+2323+ let[@inline] _is_expired ~now (e : _ entry) = e.expires_at <= now
2424+2525+ let cleanup (t : 'a t) : unit =
2626+ let now = _now_ms () in
2727+ (* collect first to avoid mutating while iterating *)
2828+ let to_remove = ref [] in
2929+ H.iter
3030+ (fun k e -> if _is_expired ~now e then to_remove := k :: !to_remove)
3131+ t.table ;
3232+ List.iter (H.remove t.table) !to_remove
3333+3434+ let _find_entry_opt (t : 'a t) (k : K.t) : 'a entry option =
3535+ try Some (H.find t.table k) with Not_found -> None
3636+3737+ let get (t : 'a t) (k : K.t) : 'a option =
3838+ let now = _now_ms () in
3939+ match _find_entry_opt t k with
4040+ | None ->
4141+ None
4242+ | Some e ->
4343+ if _is_expired ~now e then (
4444+ (* lazy eviction *)
4545+ H.remove t.table k ;
4646+ None )
4747+ else Some e.value
4848+4949+ let mem (t : 'a t) (k : K.t) : bool =
5050+ match get t k with None -> false | Some _ -> true
5151+5252+ let length (t : 'a t) : int = cleanup t ; H.length t.table
5353+5454+ let _evict_earliest (t : 'a t) : unit =
5555+ let earliest_key = ref None in
5656+ let earliest_exp = ref max_int in
5757+ H.iter
5858+ (fun k e ->
5959+ if e.expires_at < !earliest_exp then (
6060+ earliest_exp := e.expires_at ;
6161+ earliest_key := Some k ) )
6262+ t.table ;
6363+ match !earliest_key with None -> () | Some k -> H.remove t.table k
6464+6565+ let _enforce_capacity_after_insert (t : 'a t) : unit =
6666+ match t.capacity with
6767+ | None ->
6868+ ()
6969+ | Some cap ->
7070+ cleanup t ;
7171+ while H.length t.table > cap do
7272+ _evict_earliest t
7373+ done
7474+7575+ let set ?ttl_ms (t : 'a t) (k : K.t) (v : 'a) : unit =
7676+ let now = _now_ms () in
7777+ let ttl_ms = Option.value ttl_ms ~default:t.default_ttl in
7878+ if ttl_ms <= 0 then H.remove t.table k
7979+ else
8080+ let expires_at = now + ttl_ms in
8181+ let entry = {value= v; expires_at} in
8282+ H.replace t.table k entry ;
8383+ _enforce_capacity_after_insert t
8484+8585+ let replace = set
8686+8787+ let ttl_remaining_ms (t : 'a t) (k : K.t) : int option =
8888+ let now = _now_ms () in
8989+ match _find_entry_opt t k with
9090+ | None ->
9191+ None
9292+ | Some e ->
9393+ if _is_expired ~now e then (H.remove t.table k ; None)
9494+ else Some (e.expires_at - now)
9595+9696+ let to_list (t : 'a t) : (K.t * 'a) list =
9797+ cleanup t ;
9898+ let acc = ref [] in
9999+ H.iter (fun k e -> acc := (k, e.value) :: !acc) t.table ;
100100+ !acc
101101+102102+ let iter (t : 'a t) ~(f : K.t -> 'a -> unit) : unit =
103103+ cleanup t ;
104104+ H.iter (fun k e -> f k e.value) t.table
105105+106106+ let fold (t : 'a t) ~(init : 'acc) ~(f : 'acc -> K.t -> 'a -> 'acc) : 'acc =
107107+ cleanup t ;
108108+ H.fold (fun k e acc -> f acc k e.value) t.table init
109109+110110+ let set_capacity (t : 'a t) (cap : int option) : unit =
111111+ t.capacity <- cap ;
112112+ _enforce_capacity_after_insert t
113113+end
114114+115115+module String_cache = Make (struct
116116+ type t = string
117117+118118+ let equal = String.equal
119119+120120+ let hash = Hashtbl.hash
121121+end)
+87
pegasus/lib/xrpc.ml
···11+open Cohttp_lwt
22+open Cohttp_lwt_unix
33+14type init = Auth.Verifiers.ctx
2536type context = {req: Dream.request; db: Data_store.t; auth: Auth.credentials}
···2023 let%lwt body = Dream.body req in
2124 body |> Yojson.Safe.from_string |> of_yojson |> Result.get_ok |> Lwt.return
2225 with _ -> Errors.invalid_request "Invalid request body"
2626+2727+let service_proxy (ctx : context) (proxy_header : string) =
2828+ let did = Auth.get_authed_did_exn ctx.auth in
2929+ let nsid_regex =
3030+ Str.regexp
3131+ {|^[a-zA-Z](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+\.[a-zA-Z][a-zA-Z0-9]{0,62}?$|}
3232+ in
3333+ let nsid =
3434+ (Dream.path [@warning "-3"]) ctx.req
3535+ |> List.rev |> List.hd |> String.lowercase_ascii
3636+ in
3737+ if not (Str.string_match nsid_regex nsid 0) then
3838+ Errors.invalid_request "invalid nsid" ;
3939+ let service_did, service_type =
4040+ match String.split_on_char '#' proxy_header with
4141+ | [did; typ] ->
4242+ (did, typ)
4343+ | _ ->
4444+ Errors.invalid_request "invalid proxy header"
4545+ in
4646+ let fragment = "#" ^ service_type in
4747+ match%lwt Id_resolver.Did.resolve service_did with
4848+ | Ok did_doc -> (
4949+ let host =
5050+ match Id_resolver.Did.Document.get_service did_doc fragment with
5151+ | Some service ->
5252+ service
5353+ | None ->
5454+ Errors.invalid_request "failed to resolve destination service"
5555+ in
5656+ let%lwt signing_key =
5757+ match%lwt Data_store.get_actor_by_identifier did ctx.db with
5858+ | Some {signing_key; _} ->
5959+ Lwt.return signing_key
6060+ | None ->
6161+ Errors.internal_error ~msg:"user not found" ()
6262+ in
6363+ let jwt =
6464+ Auth.generate_service_jwt ~did ~service_did ~lxm:nsid ~signing_key
6565+ in
6666+ let uri =
6767+ host ^ "/" ^ String.concat "/" @@ (Dream.path [@warning "-3"]) ctx.req
6868+ |> Uri.of_string
6969+ in
7070+ let headers = Http.Header.of_list [("Authorization", "Bearer " ^ jwt)] in
7171+ match Dream.method_ ctx.req with
7272+ | `GET -> (
7373+ let%lwt res, body = Client.get uri ~headers in
7474+ match res.status with
7575+ | `OK ->
7676+ let%lwt body = Body.to_string body in
7777+ Lwt.return @@ Dream.response ~status:`OK body
7878+ | e ->
7979+ Dream.error (fun log ->
8080+ log "error when proxying to %s: %s" (Uri.to_string uri)
8181+ (Http.Status.to_string e) ) ;
8282+ Errors.internal_error ~msg:"failed to proxy request" () )
8383+ | `POST -> (
8484+ let%lwt req_body = Dream.body ctx.req in
8585+ let%lwt res, body =
8686+ Client.post uri ~headers ~body:(Body.of_string req_body)
8787+ in
8888+ match res.status with
8989+ | `OK ->
9090+ let%lwt body = Body.to_string body in
9191+ Lwt.return @@ Dream.response ~status:`OK body
9292+ | e ->
9393+ Dream.error (fun log ->
9494+ log "error when proxying to %s: %s" (Uri.to_string uri)
9595+ (Http.Status.to_string e) ) ;
9696+ Errors.internal_error ~msg:"failed to proxy request" () )
9797+ | _ ->
9898+ Errors.invalid_request "unsupported method" )
9999+ | Error _ ->
100100+ Errors.internal_error ~msg:"failed to resolve destination service" ()
101101+102102+let service_proxy_middleware db inner_handler req =
103103+ match Dream.header req "atproto-proxy" with
104104+ | Some header ->
105105+ handler ~auth:Auth.Verifiers.access
106106+ (fun ctx -> service_proxy ctx header)
107107+ {req; db}
108108+ | None ->
109109+ inner_handler req