cgi.ml 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  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. open Astring
  27. (* https://seppo.social/S1037 *)
  28. let cgi_bin = "/cgi-bin/"
  29. (* cd to asset storage location if locked inside /cgi-bin/ (if the case).
  30. *
  31. * https://seppo.social/S1037
  32. *)
  33. let cd_cgi_bin_twin_path script_name =
  34. let is_sep = Char.equal '/'
  35. and sep = "/" in
  36. match script_name |> String.fields ~is_sep with
  37. | "" :: ("cgi-bin" :: _ as l) ->
  38. let l0 = l |> List.fold_left (fun init s ->
  39. match s with
  40. | "." -> init
  41. | "seppo.cgi" -> init
  42. | ".." -> ".." :: init
  43. | _ -> ".." :: init
  44. ) [] in
  45. l |> List.fold_left (fun init s ->
  46. match s with
  47. | "."
  48. | "cgi-bin"
  49. | "seppo.cgi" -> init
  50. | s -> s :: init
  51. ) l0
  52. |> List.rev
  53. |> String.concat ~sep
  54. | _ -> (* sunshine case, not locked inside /cgi/bin/ *)
  55. "."
  56. (** HTTP Request meta data. *)
  57. module Request = struct
  58. type t = {
  59. content_type : string;
  60. content_length : int option;
  61. host : string;
  62. http_cookie : string;
  63. path_info : string;
  64. query_string : string;
  65. remote_addr : string;
  66. request_method : string;
  67. scheme : string;
  68. script_name : string;
  69. server_port : string;
  70. raw_string : string -> string option
  71. }
  72. let empty = {
  73. content_type = "text/plain";
  74. content_length = None;
  75. host = "127.0.0.1";
  76. http_cookie = "";
  77. path_info = "/";
  78. query_string = "";
  79. remote_addr = "127.0.0.1";
  80. request_method = "GET";
  81. scheme = "http";
  82. script_name = "";
  83. server_port = "80";
  84. raw_string = (fun _ -> None);
  85. }
  86. (** Request meta data. https://tools.ietf.org/html/rfc3875#section-4.1.13 *)
  87. (* https://tools.ietf.org/html/rfc3875 *)
  88. let hCONTENT_LENGTH = "CONTENT_LENGTH"
  89. let hCONTENT_TYPE = "CONTENT_TYPE"
  90. let hHTTP_COOKIE = "HTTP_COOKIE"
  91. let hHTTP_HOST = "HTTP_HOST"
  92. let hHTTP_USER_AGENT = "HTTP_USER_AGENT"
  93. let hHTTPS = "HTTPS"
  94. let hPATH_INFO = "PATH_INFO"
  95. let hQUERY_STRING = "QUERY_STRING"
  96. let hREMOTE_ADDR = "REMOTE_ADDR"
  97. let hREQUEST_METHOD = "REQUEST_METHOD"
  98. let hREQUEST_URI = "REQUEST_URI"
  99. let hSCRIPT_NAME = "SCRIPT_NAME"
  100. let hSERVER_NAME = "SERVER_NAME"
  101. let hSERVER_PORT = "SERVER_PORT"
  102. let hHTTP_X_FORWARDED_FOR = "HTTP_X_FORWARDED_FOR"
  103. let hHTTP_X_FORWARDED_PROTO = "HTTP_X_FORWARDED_PROTO"
  104. (** Almost trivial. https://tools.ietf.org/html/rfc3875
  105. * Does no parsing or conversion. *)
  106. let from_env ?(getenv_opt=Sys.getenv_opt) () =
  107. try
  108. let env_opt ?(default = "") s = s |> getenv_opt |> Option.value ~default in
  109. let env_exc s =
  110. match s |> getenv_opt with
  111. | None -> raise Not_found
  112. | Some v -> v in
  113. let r : t = {
  114. content_type = hCONTENT_TYPE |> env_opt;
  115. content_length = Option.bind
  116. (hCONTENT_LENGTH |> getenv_opt)
  117. (fun s -> Option.bind
  118. (s |> int_of_string_opt)
  119. Option.some);
  120. host = hHTTP_HOST |> env_opt ~default:(hSERVER_NAME |> env_exc);
  121. http_cookie = hHTTP_COOKIE |> env_opt;
  122. path_info = hPATH_INFO |> env_opt;
  123. query_string = hQUERY_STRING |> env_opt;
  124. request_method = hREQUEST_METHOD|>env_exc;
  125. remote_addr = hREMOTE_ADDR |> env_exc;
  126. (* request_uri = hREQUEST_URI |> Os.getenv ; *)
  127. scheme = (match hHTTPS |> env_opt with
  128. | "on" -> "https"
  129. | _ -> "http");
  130. script_name = hSCRIPT_NAME |> env_exc;
  131. server_port = hSERVER_PORT |> env_exc;
  132. raw_string = getenv_opt (* mybe we should limit and HTTP_ prefix the names *)
  133. }
  134. in Ok r
  135. with Not_found -> Error "Not Found."
  136. (** despite https://tools.ietf.org/html/rfc3875#section-4.1.13 1und1.de
  137. webhosting returns the script_name instead an empty or None path_info in
  138. case *)
  139. let consolidate req' =
  140. Result.bind req' (fun (req : t) ->
  141. if String.equal req.path_info req.script_name
  142. then Ok {req with path_info = ""}
  143. else req')
  144. (** use remote_addr, scheme and server_port according to proxy *)
  145. let proxy req' =
  146. Result.bind req' (fun (req : t) ->
  147. match hHTTP_X_FORWARDED_FOR |> req.raw_string with
  148. | None -> req'
  149. | Some remote_addr ->
  150. let req = {req with remote_addr} in
  151. match hHTTP_X_FORWARDED_PROTO |> req.raw_string with
  152. | None -> Ok req
  153. | Some scheme ->
  154. let req = {req with scheme} in
  155. Ok (match scheme with
  156. | "https" -> {req with server_port = "443" }
  157. | "http" -> {req with server_port = "80" }
  158. | _ -> req ))
  159. (** compute scheme, host, port *)
  160. let srvr r : Uri.t =
  161. let u = Uri.make
  162. ~scheme:r.scheme
  163. ~host:r.host
  164. () in
  165. let port = match r.scheme, r.server_port with
  166. | "http" , "80" -> None
  167. | "https", "443" -> None
  168. | _, p -> Some (p |> int_of_string)
  169. in
  170. Uri.with_port u port
  171. let rx_cgi_bin = {|^/cgi-bin\(\(/.*\)seppo\.cgi\)|} |> Str.regexp
  172. let rx_script_name = {|^\(/cgi-bin\)?\(\(/\([^/]*/\)*\)\([^/]*\.cgi\)\)$|} |> Str.regexp
  173. let script_url s =
  174. Logr.debug (fun m -> m "%s.%s %s" "Cgi" "script_url" s);
  175. if Str.string_match rx_cgi_bin s 0
  176. then s |> Str.matched_group 1
  177. else s
  178. let script_url_dir s =
  179. Logr.debug (fun m -> m "%s.%s %s" "Cgi" "script_url_dir" s);
  180. let b = if Str.string_match rx_script_name s 0
  181. then s |> Str.matched_group 3
  182. else failwith __LOC__ in
  183. assert (b |> St.is_suffix ~affix:"/");
  184. assert (not (b |> St.is_suffix ~affix:"//"));
  185. assert (not (b |> St.is_prefix ~affix:cgi_bin));
  186. b
  187. (** set script and path for a query_string. *)
  188. let path_and_query r =
  189. let path = (r.script_name |> script_url) ^ r.path_info in
  190. let u = Uri.make ~path () in
  191. match r.query_string with
  192. | "" -> u
  193. | q -> q |> Uri.query_of_encoded |> Uri.with_query u
  194. let base' script_name srvr : Uri.t =
  195. assert (srvr |> Uri.path |> String.equal "");
  196. script_name
  197. |> script_url_dir
  198. |> Uri.with_path srvr
  199. let base r =
  200. r |> srvr |> base' r.script_name
  201. (** absolute request-uri, without /cgi-bin/ in case *)
  202. let abs r : Uri.t =
  203. let u = r |> srvr in
  204. let u = (r.script_name |> script_url) ^ r.path_info |> Uri.with_path u in
  205. match r.query_string with
  206. | "" -> u
  207. | q -> q |> Uri.query_of_encoded |> Uri.with_query u
  208. (** fetch http header values and map from lowercase plus the special name (request-target) *)
  209. let header_get (r : t) = function
  210. | "(request-target)" -> Printf.sprintf "%s %s"
  211. (r.request_method |> String.Ascii.lowercase)
  212. (r |> path_and_query |> Uri.to_string)
  213. |> Option.some
  214. | k ->
  215. let toenv = String.map (function
  216. | '-' -> '_'
  217. | c -> Char.Ascii.uppercase c) in
  218. match toenv k with
  219. | "CONTENT_LENGTH"
  220. | "CONTENT_TYPE" as k -> k |> r.raw_string
  221. | k -> ("HTTP_" ^ k) |> r.raw_string
  222. end
  223. module Response = struct
  224. (** return type of the Request handlers. *)
  225. type t = Cohttp.Code.status_code * (string * string) list * (out_channel -> unit)
  226. (** for ease of railway processing we use this strange type *)
  227. type t' = (t, t) result
  228. let body ?(ee = "") b oc =
  229. output_string oc b;
  230. if ee != "" then (
  231. output_string oc "\n\n";
  232. output_string oc ee)
  233. let nobody = St.camel |> body
  234. let flush uuid oc ((status, hdrs, f_body) : t) : int =
  235. Logr.debug (fun m -> m "%s.%s %a" "Cgi.Response" "flush" Uuidm.pp uuid);
  236. let single (k, v) = Printf.fprintf oc "%s: %s\r\n" k v in
  237. ("Status", status |> Cohttp.Code.string_of_status) |> single;
  238. hdrs |> List.iter single;
  239. ("X-Request-Id", uuid |> Uuidm.to_string) |> single;
  240. Printf.fprintf oc "\r\n";
  241. f_body oc;
  242. flush oc;
  243. match status with
  244. | #Cohttp.Code.server_error_status -> 1
  245. | _ -> 0
  246. end