cgi.ml 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. (*
  2. * _ _ ____ _
  3. * _| || |_/ ___| ___ _ __ _ __ ___ | |
  4. * |_ .. _\___ \ / _ \ '_ \| '_ \ / _ \| |
  5. * |_ _|___) | __/ |_) | |_) | (_) |_|
  6. * |_||_| |____/ \___| .__/| .__/ \___/(_)
  7. * |_| |_|
  8. *
  9. * Personal Social Web.
  10. *
  11. * cgi.ml
  12. *
  13. * Copyright (C) The #Seppo contributors. All rights reserved.
  14. *
  15. * This program is free software: you can redistribute it and/or modify
  16. * it under the terms of the GNU General Public License as published by
  17. * the Free Software Foundation, either version 3 of the License, or
  18. * (at your option) any later version.
  19. *
  20. * This program is distributed in the hope that it will be useful,
  21. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  22. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  23. * GNU General Public License for more details.
  24. *
  25. * You should have received a copy of the GNU General Public License
  26. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  27. *)
  28. open Seppo_lib
  29. open Astring
  30. let ( let* ) = Result.bind
  31. let ( >>= ) = Result.bind
  32. let ( >>| ) a b = match a with
  33. | Error _ as e -> Lwt.return e
  34. | Ok a -> b a
  35. let post_limit_b = 2 * 1024
  36. (* Handle incoming HTTP requests.
  37. *
  38. * Has the
  39. * - brue force mitigation ban,
  40. * - initial setup,
  41. * - asset restore,
  42. * - URL router (dispatch),
  43. * - session enforcement
  44. * - /ping loop
  45. * and delegates to the logic in Iweb (UI webinterface) or Is2s (ActivityPub Server to Server endpoint)
  46. *
  47. * Still does a Lwt_main.run that preferably owuld be outside
  48. *)
  49. let handle uuid tnow ic (req : Cgi.Request.t) : Cgi.Response.t =
  50. let t0 = Sys.time() in
  51. Logr.debug (fun m -> m "%s.%s %a %s %s %s" "Cgi" "handle" Uuidm.pp uuid req.remote_addr req.request_method req.path_info);
  52. assert (not (req.path_info |> St.is_prefix ~affix:("/" ^ Cfg.seppo_cgi)));
  53. (** redirect to password reset if non exists *)
  54. let redir_if_passwd_nonex (r : Cgi.Request.t) =
  55. let loc = Iweb.Passwd.path in
  56. if Auth.fn |> File.exists
  57. || r.path_info |> String.equal loc
  58. then Ok r
  59. else
  60. (* start a 'recovery' session *)
  61. let* _,sec = Cfg.ServerSession.create tnow
  62. |> Option.to_result ~none:Http.s500' in
  63. let header = [ Iweb.ClientCookie.new_session ~tnow sec req Auth.dummy ] in
  64. Cfg.seppo_cgi ^ loc |> Http.s302 ~header
  65. and restore_assets lst r =
  66. let _ = Assets.Const.restore_if_nonex File.pFile lst in
  67. Ok r
  68. and
  69. (** URL router and HTTP middleware. *)
  70. dispatch (r : Cgi.Request.t) =
  71. (* Logr.debug (fun m -> m "%s.%s path_info '%s'" "Cgi" "handle.dispatch" r.path_info); *)
  72. let (* send_file ct p = p
  73. |> File.to_string
  74. |> Http.clob_send uuid ct
  75. and *) (** Send an asset from inside the binary *)
  76. send_res ct p = match p |> Res.read with
  77. | None -> Http.s500
  78. | Some b -> Http.clob_send uuid ct b
  79. and send_res' ct (Auth.Uid _,(r : Cgi.Request.t)) =
  80. match match r.path_info with
  81. | "/people" as p -> p ^ ".xml" |> Res.read
  82. | _ -> None with
  83. | None -> Http.s404
  84. | Some s -> Http.clob_send uuid ct s
  85. and ases = Iweb.ases tnow
  86. and auth = Iweb.uid_redir
  87. and ban = Ban.escalate Ban.cdb
  88. and base () = (* lazy, may not exist yet *) Cfg.Base.(from_file fn) |> Result.get_ok
  89. and csrf_ck v = Iweb.Token.(check fn v)
  90. and csrf_mk v = Ok Iweb.Token.(create ~uuid fn, v)
  91. and form (r : Cgi.Request.t) ic v =
  92. match r.content_length with
  93. | None -> Http.s411
  94. | Some n -> if n < 0 || n > post_limit_b
  95. then Http.s413
  96. else try
  97. Ok (ic |> Html.Form.of_channel n, v)
  98. with _ -> Http.s400
  99. and rt = Lwt.return
  100. and tz = Timedesc.Time_zone.(local () |> Option.value ~default:utc)
  101. and s302
  102. ?(qs="")
  103. ?(header = [])
  104. p =
  105. r.script_name ^ p ^ qs
  106. |> Http.s302 ~header in
  107. let re = match r.path_info, r.request_method |> Cohttp.Code.method_of_string with
  108. | ("/doap.rdf" as p, `GET) -> p |> send_res Http.Mime.text_xml |> rt
  109. | ("/LICENSE" as p, `GET)
  110. (*
  111. | ("/var/lock/challenge" as p, `GET) -> let f = "app" ^ p in f |> send_file Http.Mime.text_plain |> rt
  112. *)
  113. | ("/version" as p, `GET) -> p |> send_res Http.Mime.text_plain |> rt
  114. | "/activitypub/actor.xml", `GET -> r |> ases >>= auth >>= csrf_mk >>| Iweb.Actor.get ~base uuid
  115. | "/activitypub/actor.xml", `POST -> r |> ases >>= auth >>= form r ic >>= csrf_ck >>| Iweb.Actor.post ~base uuid tnow
  116. | "/activitypub/actor.xml/icon", `GET -> r |> Iweb.Actor.Icon.get ~base uuid
  117. | "/activitypub/announce", `GET -> r |> ases >>= auth >>= csrf_mk >>= Iweb.Announce.get ~base uuid tnow |> rt
  118. | "/activitypub/dislike", `GET -> r |> ases >>= auth >>= csrf_mk >>= Iweb.Like.get ~undo:true ~base uuid tnow |> rt
  119. | "/activitypub/inbox.jsa", `POST -> r |> Is2s.Inbox.post ~base uuid tnow ic
  120. | "/activitypub/like", `GET -> r |> ases >>= auth >>= csrf_mk >>= Iweb.Like.get ~base uuid tnow |> rt
  121. | "/backoffice/", `GET -> r |> ases >>= auth >>= Iweb.Health.get ~base uuid |> rt
  122. | "/http", `GET -> r |> ases >>= auth >>| Iweb.Http_.get ~base uuid tnow
  123. | "/login", `GET -> r |> csrf_mk >>= Iweb.Login.get uuid |> rt
  124. | "/login", `POST -> r |> form r ic >>= csrf_ck >>= Iweb.Login.post uuid tnow ban |> rt
  125. | "/logout", `GET -> r |> ases >>= Iweb.Logout.get uuid |> rt
  126. (*
  127. | "/note", `GET -> r |> ases >>= auth >>= csrf_mk >>= Iweb.Note.get uuid |> rt
  128. *)
  129. | "/notifyme", `GET -> r |> Result.ok >>| Iweb.Notifyme.get ~base uuid tnow
  130. | "/passwd", `GET -> r |> ases >>= auth >>= csrf_mk >>= Iweb.Passwd.get uuid |> rt
  131. | "/passwd", `POST -> r |> ases >>= auth >>= form r ic >>= csrf_ck >>= Iweb.Passwd.post uuid tnow |> rt
  132. | "/people", `GET -> r |> ases >>= auth >>= send_res' Http.Mime.text_xml |> rt
  133. | "/ping", `GET -> r |> Iweb.Ping.get ~base uuid
  134. | "/edit", `GET
  135. | "/post", `GET -> r |> ases >>= auth >>= csrf_mk >>= Iweb.Post.get ~base uuid |> rt
  136. | "/edit", `POST
  137. | "/post", `POST -> r |> ases >>= auth >>= form r ic >>= csrf_ck >>| Iweb.Post.post ~base uuid tnow
  138. | "/profile", `GET -> r |> ases >>= auth >>= csrf_mk >>= Iweb.Profile.get uuid |> rt
  139. | "/profile", `POST -> r |> ases >>= auth >>= form r ic >>= csrf_ck >>= Iweb.Profile.post uuid tnow |> rt
  140. | "/search", `GET -> r |> ases >>= auth >>= Iweb.Search.get ~base uuid |> rt
  141. | "/session", `GET -> r |> ases >>= Iweb.Session.get uuid |> rt
  142. | "/timeline/", `GET -> "p/" |> Http.s302 |> rt
  143. | "/tools", `GET -> Http.s501 |> rt
  144. | "/tools", `POST -> Http.s501 |> rt
  145. | "/webfinger", `GET -> r |> Iweb.Webfing.get uuid
  146. | "/", `GET -> ".." |> Http.s302 |> rt
  147. | "", `GET when "" = r.query_string -> Http.s302 "." |> rt
  148. | "", `GET -> (let ur = r |> Cgi.Request.path_and_query in
  149. (* shaarli compatibility *)
  150. match "do" |> Uri.get_query_param ur with
  151. | Some "login" -> s302 Iweb.Login.path
  152. | Some "logout" -> s302 Iweb.Logout.path
  153. | Some "configure" -> s302 Iweb.Profile.path
  154. | _ ->
  155. (* accessing random urls leads to a ban, eventually *)
  156. ban tnow r.remote_addr;
  157. Http.s404
  158. ) |> rt
  159. | _, `GET when r |> Iweb.Timeline.can_handle -> r |> ases >>= auth >>= Iweb.Timeline.get ~tz ~base uuid tnow |> rt
  160. | _, `GET when r |> Iweb.Webfing.can_handle ~prefix:"/@" -> r |> Iweb.Webfing.do_handle ~prefix:"/@" |> rt
  161. | _, `GET when r |> Iweb.Webfing.can_handle ~prefix:"/acct:" -> r |> Iweb.Webfing.do_handle ~prefix:"/acct:" |> rt
  162. | _, `HEAD -> Http.s405 |> rt
  163. | _ ->
  164. (* accessing random urls leads to a ban, eventually *)
  165. ban tnow r.remote_addr;
  166. Http.s404 |> rt in
  167. re |> Lwt_main.run
  168. and
  169. (** Unite Ok and Error and write response. *)
  170. merge (x : (Cgi.Response.t, Cgi.Response.t) result) : Cgi.Response.t =
  171. let (status,_,_) as x = match x with
  172. | Ok x -> x
  173. | Error x -> x in
  174. Logr.info (fun m -> m "%s.%s %a dt=%.3fs HTTP %s %s %s -> localhost%a"
  175. "Cgi" "handle"
  176. Uuidm.pp uuid
  177. (Sys.time() -. t0)
  178. (status |> Cohttp.Code.string_of_status)
  179. req.request_method
  180. req.remote_addr
  181. Uri.pp (req |> Cgi.Request.path_and_query));
  182. x in
  183. Ok req
  184. >>= Ban.(check_req (prepare_cdb cdb) tnow)
  185. >>= Iweb.redir_if_cgi_bin
  186. >>= Assets.Const.(restore_assets all)
  187. >>= redir_if_passwd_nonex
  188. >>= dispatch
  189. |> merge