diff --git a/CHANGES b/CHANGES index cb081f52f..4baf798bc 100644 --- a/CHANGES +++ b/CHANGES @@ -3,6 +3,9 @@ "ocsigenserver --serve ./public --port 8000 --directory-listing") now serves a directory of static files without any configuration file or pre-created log directory. The Staticmod extension is loaded on demand. + * One-command reverse proxy: "ocsigenserver --reverse-proxy URL" forwards + every request to the base URL given, with no configuration file. The + Revproxy extension is loaded on demand. * Logging: when no (and no ) is given in the configuration file, logs now go to the standard output and standard error instead of to files, so that no log directory is required. diff --git a/doc/index.mld b/doc/index.mld index 325b8aab9..2befbccc5 100644 --- a/doc/index.mld +++ b/doc/index.mld @@ -34,9 +34,15 @@ terminal. A few options are available: {v ocsigenserver --serve ./public --port 8000 --directory-listing v} +You can also start a reverse proxy that forwards every request to another +server, again without any configuration file: +{v +ocsigenserver --reverse-proxy http://localhost:9000 --port 8080 +v} + This is the simplest way to get started. For anything more elaborate (several -hosts, extensions, reverse proxy, etc.) use a configuration file or the library -API, as described below. +hosts, extensions, fine-grained reverse proxy rules, etc.) use a configuration +file or the library API, as described below. {1 Using Ocsigen Server as an executable} diff --git a/doc/launching.mld b/doc/launching.mld index cda2f2887..97121f0aa 100644 --- a/doc/launching.mld +++ b/doc/launching.mld @@ -11,6 +11,10 @@ following options: file (logs go to the terminal). DIR can also be given as a plain positional argument: "ocsigenserver ./public". + --reverse-proxy URL + Forward every request to the base URL given (for example + http://localhost:9000), without a configuration file. + -P, --port Port to listen on in serve mode (default 8080). diff --git a/src/extensions/revproxy.ml b/src/extensions/revproxy.ml index d521ab3e2..41c4a5f5d 100644 --- a/src/extensions/revproxy.ml +++ b/src/extensions/revproxy.ml @@ -192,3 +192,14 @@ let () = () let run ~redirection () _ _ _ = gen redirection + +(* Publish a "forward everything to TARGET" function so that the one-command + reverse-proxy mode can use it after loading this extension dynamically, + without a static dependency. *) +let () = + Ocsigen.Server.register_reverse_proxy_server (fun ~target -> + let target = Ocsigen_base.Lib.Url.remove_end_slash target in + run + ~redirection: + (create_redirection ~full_url:false ~regexp:"(.*)" (target ^ "/\\1")) + ()) diff --git a/src/ocsigenserver.ml b/src/ocsigenserver.ml index 0a4d0a44b..c45b2f9a7 100644 --- a/src/ocsigenserver.ml +++ b/src/ocsigenserver.ml @@ -1,4 +1,6 @@ -let usage = "usage: ocsigenserver [-c configfile | [--serve] DIR [options]]" +let usage = + "usage: ocsigenserver [-c configfile | [--serve] DIR | --reverse-proxy URL [options]]" + let section = Logs.Src.create "ocsigen:main" (* Report command-line errors through Logs. A stderr reporter is installed @@ -19,6 +21,7 @@ let fatal fmt = let serve_dir = ref None let serve_port = ref None let directory_listing = ref false +let reverse_proxy = ref None let config_given = ref false let set_serve_dir d = @@ -35,6 +38,8 @@ let () = let alt_msg = "Alternate config file (default " ^ Ocsigen.Config.get_config_file () ^ ")" and serve_msg = "Serve the static files of DIR without a configuration file" + and proxy_msg = + "Forward every request to the base URL given, without a configuration file" and port_msg = "Port to listen on in serve mode (default 8080)" and listing_msg = "List directory contents in serve mode (when no index file)" and silent_msg = "Silent mode (error messages in errors.log only)" @@ -49,6 +54,9 @@ let () = [ "-c", Arg.String set_configfile, alt_msg ; "--config", Arg.String set_configfile, alt_msg ; "--serve", Arg.String set_serve_dir, serve_msg + ; ( "--reverse-proxy" + , Arg.String (fun u -> reverse_proxy := Some u) + , proxy_msg ) ; "-P", Arg.Int (fun p -> serve_port := Some p), port_msg ; "--port", Arg.Int (fun p -> serve_port := Some p), port_msg ; "--directory-listing", Arg.Set directory_listing, listing_msg @@ -86,14 +94,23 @@ let check_serve_dir dir = | exception Sys_error _ -> fatal "no such directory: %s" dir let () = - match !serve_dir with - | Some dir -> + match !serve_dir, !reverse_proxy with + | Some _, Some _ -> fatal "--serve and --reverse-proxy cannot be combined" + | Some dir, None -> if !config_given then fatal "-c/--config cannot be combined with serve mode"; let dir = check_serve_dir dir in Ocsigen.Server.serve ~dir ?port:!serve_port ~directory_listing:!directory_listing () - | None -> + | None, Some target -> + if !config_given + then fatal "-c/--config cannot be combined with --reverse-proxy"; + if !directory_listing + then fatal "--directory-listing is only valid when serving a directory"; + Ocsigen.Server.reverse_proxy ~target ?port:!serve_port () + | None, None -> if !serve_port <> None || !directory_listing - then fatal "--port and --directory-listing require a directory to serve"; + then + fatal + "--port and --directory-listing require a directory to serve or --reverse-proxy"; Ocsigen.Server.exec (Ocsigen.Parseconfig.parse_config ()) diff --git a/src/server/Ocsigen/server.ml b/src/server/Ocsigen/server.ml index 5008cf87c..2da67fb8b 100644 --- a/src/server/Ocsigen/server.ml +++ b/src/server/Ocsigen/server.ml @@ -475,39 +475,55 @@ let start Extensions.start_initialisation (); Extensions.set_hosts instructions) -(* Registry through which the Staticmod extension, once loaded, publishes its - static-file serving function. This lets the one-command serve mode use the - extension without statically linking it (which would clash with the - configuration-file path, where the extension is loaded dynamically). *) +(* Registries through which the Staticmod and Revproxy extensions, once loaded, + publish their serving functions. This lets the one-command serve mode use + them without statically linking them (which would clash with the + configuration-file path, where the extensions are loaded dynamically). *) let static_server : (dir:string -> instruction) option ref = ref None let register_static_server f = static_server := Some f +let reverse_proxy_server : (target:string -> instruction) option ref = ref None +let register_reverse_proxy_server f = reverse_proxy_server := Some f -let serve ?(port = 8080) ?(directory_listing = false) ~dir () = - (* One-command serve mode: no configuration file and no log directory are - required. Logs go to stderr and the command pipe is placed in a temporary - location so that the usual control commands remain available. *) +(* Common setup of the one-command modes: no configuration file and no log + directory are required. Logs go to stderr and the command pipe is placed in a + temporary location so that the usual control commands remain available. *) +let one_command_setup () = Config.set_log_to_stderr true; Config.set_command_pipe (Filename.concat (Filename.get_temp_dir_name ()) - (Printf.sprintf "ocsigenserver-%d.cmd" (Unix.getpid ()))); - (* Load the Staticmod extension on demand. It registers its serving function - through [register_static_server]. *) + (Printf.sprintf "ocsigenserver-%d.cmd" (Unix.getpid ()))) + +(* Load the extension [package] on demand and return the serving function it + published in [registry]. *) +let load_one_command_extension package registry = (try Ocsigen_base.Loader.loadfiles (fun () -> ()) (fun () -> ()) false - (Ocsigen_base.Loader.findfiles "ocsigenserver.ext.staticmod") + (Ocsigen_base.Loader.findfiles package) with e -> let msg, errno = errmsg e in Messages.errlog msg; exit errno); - match !static_server with + match !registry with + | Some f -> f | None -> - Messages.errlog - "The Staticmod extension did not register its serving function"; + Messages.errlog (package ^ " did not register its serving function"); exit 1 - | Some static -> - start - ~ports:[`All, port] - [host ~list_directory_content:directory_listing [static ~dir]] + +let serve ?(port = 8080) ?(directory_listing = false) ~dir () = + one_command_setup (); + let static = + load_one_command_extension "ocsigenserver.ext.staticmod" static_server + in + start + ~ports:[`All, port] + [host ~list_directory_content:directory_listing [static ~dir]] + +let reverse_proxy ?(port = 8080) ~target () = + one_command_setup (); + let proxy = + load_one_command_extension "ocsigenserver.ext.revproxy" reverse_proxy_server + in + start ~ports:[`All, port] [host [proxy ~target]] diff --git a/src/server/Ocsigen/server.mli b/src/server/Ocsigen/server.mli index ad125b2bd..8bcc334fd 100644 --- a/src/server/Ocsigen/server.mli +++ b/src/server/Ocsigen/server.mli @@ -37,6 +37,13 @@ val serve : ?port:int -> ?directory_listing:bool -> dir:string -> unit -> unit into the program. [port] defaults to 8080. When [directory_listing] is [true], directories without an index file are listed. Never returns. *) +val reverse_proxy : ?port:int -> target:string -> unit -> unit +(** [reverse_proxy ~target ()] starts a server that forwards every request to + the [target] base URL (for example [http://localhost:9000]), with no + configuration file and no log directory required. This backs + [ocsigenserver --reverse-proxy URL]. The Revproxy extension is loaded + dynamically on demand. [port] defaults to 8080. Never returns. *) + val start : ?ports:(Config.Socket_type.t * int) list -> ?ssl_ports:(Config.Socket_type.t * int) list @@ -94,6 +101,11 @@ val register_static_server : (dir:string -> instruction) -> unit static-file serving function, so that {!serve} can use it without a static dependency on the extension. *) +val register_reverse_proxy_server : (target:string -> instruction) -> unit +(** Called by the Revproxy extension when it is loaded to publish its + request-forwarding function, so that {!reverse_proxy} can use it without a + static dependency on the extension. *) + val host : ?regexp:string -> ?port:int diff --git a/test/reverse-proxy.t/run.t b/test/reverse-proxy.t/run.t new file mode 100644 index 000000000..14027d0e1 --- /dev/null +++ b/test/reverse-proxy.t/run.t @@ -0,0 +1,28 @@ +Forward every request to another server with a single command, with no +configuration file. + +Start an upstream server that serves a directory: + + $ mkdir upstream + $ printf 'hello from upstream' > upstream/hello.txt + $ ocsigenserver --serve upstream --port 8071 >upstream.log 2>&1 & + $ UPSTREAM_PID=$! + $ trap 'kill $UPSTREAM_PID $PROXY_PID 2>/dev/null' EXIT + +Wait for the upstream to answer: + + $ curl -s --retry 20 --retry-delay 1 --retry-connrefused \ + > http://127.0.0.1:8071/hello.txt + hello from upstream (no-eol) + +Start a reverse proxy in front of it: + + $ ocsigenserver --reverse-proxy http://127.0.0.1:8071 --port 8072 \ + > >proxy.log 2>&1 & + $ PROXY_PID=$! + +A request to the proxy is forwarded to the upstream and its answer is relayed: + + $ curl -s --retry 20 --retry-delay 1 --retry-connrefused \ + > http://127.0.0.1:8072/hello.txt + hello from upstream (no-eol)