diff --git a/README.md b/README.md index 1fc8272..db61370 100644 --- a/README.md +++ b/README.md @@ -23,31 +23,31 @@ `phd` is a small, easy-to-use gopher server. -Point it at a directory and it'll serve up all its text files, +Point it at a directory and it'll serve up all the text files, sub-directories, and binary files over Gopher. Any `.gph` files will be served up as [gopermaps][map] and executable `.gph` files will be run as a program with their output served to the client, like the glorious cgi-bin days of yore! -### special files: +### ~ special files ~ - **header.gph**: If it exists in a directory, its content will be shown above the directory's content. Put ASCII art in it. - **footer.gph**: Same, but will be shown below a directory's content. - **index.gph**: Completely replaces a directory's content with what's in this file. -- **??.gph**: Visiting gopher://yoursite/1/dog/ will try to render - `dog.gph` from disk. Visiting /1/dog.gph will render the raw content - of the .gph file. +- **??.gph**: Visiting `gopher://yoursite/1/dog/` will try to render + `dog.gph` from disk. Visiting `/1/dog.gph` will render the raw + content of the .gph file. - **.reverse**: If this exists, the directory contents will be listed in reverse alphanumeric order. Useful for phloggin', if you date your posts. -Any line in a `.gph` file that doesn't contain tabs (`\t`) and doesn't -start with an `i` will get an `i` automatically prefixed, turning it -into a gopher information item. +Any line in a `.gph` file that doesn't contain tabs (`\t`) will get an +`i` automatically prefixed, turning it into a Gopher information item. -Alternatively, phd supports [geomyidae][gmi] syntax: +For your convenience, phd supports **[geomyidae][gmi]** syntax for +creating links: This is an info line. [1|This is a link|/help|server|port] @@ -56,7 +56,10 @@ Alternatively, phd supports [geomyidae][gmi] syntax: `server` and `port` will get translated into the server and port of the actively running server, eg `localhost` and `7070`. -### dynamic content: +Any line containing a tab character (`\t`) will be sent as-is to the +client, meaning you can write and serve up raw Gophermap files too. + +### ~ dynamic content ~ Any `.gph` file that is marked **executable** with be run as if it were a standalone program and its output will be sent to the client. @@ -92,7 +95,7 @@ then: [INFO] |_| |_|_| \__, |\___/| .__/|_| |_|\___|_| [INFO] |___/ |_| -### ruby on rails: +### ~ ruby on rails ~ `sh` is fun, but for serious work you need a serious scripting language like Ruby or PHP or Node.JS: @@ -127,14 +130,17 @@ of Gopher! isizes.gph 276B (null) 127.0.0.1 7070 isrc 224B (null) 127.0.0.1 7070 -## usage +## ~ usage ~ + + Usage: phd [options] Options: - -p, --port Port to bind to. - -h, --host Hostname to use when generating links. + -r, --render SELECTOR Render and print SELECTOR to stdout only. + -p, --port Port to bind to. [Default: {port}] + -h, --host Hostname for links. [Default: {host}] Other flags: @@ -146,9 +152,10 @@ of Gopher! phd ./path/to/site # Serve directory over port 7070. phd -p 70 docs # Serve 'docs' directory on port 70 phd -h gopher.com # Serve current directory over port 7070 - # using hostname "gopher.com" + # using hostname 'gopher.com' + phd -r / ./site # Render local gopher site to stdout. -## installation +## ~ installation ~ On macOS you can install with [Homebrew](https://brew.sh/): @@ -161,22 +168,25 @@ gopher://phkt.io/1/releases/phd and https://github.com/xvxx/phd/releases: - [phd-v0.1.9-linux-armv7.tar.gz (Raspberry Pi)][1] - [phd-v0.1.9-macos.zip][2] -Just unzip/untar the `phd` program into your \$PATH and get going! +Just unzip/untar the `phd` program into your `$PATH` and get going! + +If you have **[cargo][rustup]**, you can install the crate directly: + + cargo install phd -## development +## ~ development ~ cargo run -- ./path/to/gopher/site -## resources +## ~ resources ~ - gopher://bitreich.org/1/scm/geomyidae/files.gph - https://github.com/gophernicus/gophernicus/blob/master/README.Gophermap - https://gopher.zone/posts/how-to-gophermap/ - [rfc 1436](https://tools.ietf.org/html/rfc1436) -## todo +## ~ todo ~ -- [ ] script/serverless mode - [ ] systemd config, or something - [ ] TLS support - [ ] man page @@ -187,4 +197,5 @@ Just unzip/untar the `phd` program into your \$PATH and get going! [1]: https://github.com/xvxx/phd/releases/download/v0.1.9/phd-v0.1.9-linux-armv7.tar.gz [2]: https://github.com/xvxx/phd/releases/download/v0.1.9/phd-v0.1.9-macos.zip [map]: https://en.wikipedia.org/wiki/Gopher_(protocol)#Source_code_of_a_menu -[gmi]: gopher://bitreich.org/1/scm/geomyidae +[gmi]: http://r-36.net/scm/geomyidae/ +[rustup]: https://rustup.rs diff --git a/src/main.rs b/src/main.rs index 645ee14..38a4d5f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,17 +5,25 @@ const DEFAULT_HOST: &str = "127.0.0.1"; const DEFAULT_PORT: u16 = 7070; fn main() { - let args: Vec = std::env::args().skip(1).collect(); + let args = std::env::args().skip(1).collect::>(); + let mut args = args.iter(); let mut root = "."; - let mut iter = args.iter(); let mut host = DEFAULT_HOST; let mut port = DEFAULT_PORT; - while let Some(arg) = iter.next() { + let mut render = ""; + while let Some(arg) = args.next() { match arg.as_ref() { "--version" | "-v" | "-version" => return print_version(), "--help" | "-help" => return print_help(), + "--render" | "-render" | "-r" => { + if let Some(path) = args.next() { + render = path; + } else { + render = "/"; + } + } "--port" | "-p" | "-port" => { - if let Some(p) = iter.next() { + if let Some(p) = args.next() { port = p .parse() .map_err(|_| { @@ -26,15 +34,15 @@ fn main() { } } "-h" => { - if let Some(h) = iter.next() { - host = h; + if let Some(h) = args.next() { + host = &h; } else { return print_help(); } } "--host" | "-host" => { - if let Some(h) = iter.next() { - host = h; + if let Some(h) = args.next() { + host = &h; } } _ => { @@ -42,12 +50,19 @@ fn main() { eprintln!("unknown flag: {}", arg); process::exit(1); } else { - root = arg; + root = &arg; } } } } + if !render.is_empty() { + return match phd::server::render(host, port, root, &render) { + Ok(out) => println!("{}", out), + Err(e) => eprintln!("{}", e), + }; + } + if let Err(e) = phd::server::start(host, port, root) { eprintln!("{}", e); } @@ -61,13 +76,23 @@ fn print_help() { Options: - -p, --port Port to bind to. [Default: {port}] - -h, --host Hostname when generating links. [Default: {host}] + -r, --render SELECTOR Render and print SELECTOR to stdout only. + -p, --port Port to bind to. [Default: {port}] + -h, --host Hostname for links. [Default: {host}] Other flags: -h, --help Print this screen. - -v, --version Print phd version.", + -v, --version Print phd version. + +Examples: + + phd ./path/to/site # Serve directory over port 7070. + phd -p 70 docs # Serve 'docs' directory on port 70 + phd -h gopher.com # Serve current directory over port 7070 + # using hostname 'gopher.com' + phd -r / ./site # Render local gopher site to stdout. +", host = DEFAULT_HOST, port = DEFAULT_PORT, ); diff --git a/src/server.rs b/src/server.rs index 7f8b5ee..1dfa583 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,7 +1,7 @@ //! A simple multi-threaded Gopher server. use crate::{color, Request, Result}; -use gophermap::{GopherMenu, ItemType}; +use gophermap::ItemType; use std::{ cmp::Ordering, fs::{self, DirEntry}, @@ -62,7 +62,7 @@ pub fn start(host: &str, port: u16, root: &str) -> Result<()> { } /// Reads from the client and responds. -fn accept(stream: TcpStream, mut req: Request) -> Result<()> { +fn accept(mut stream: TcpStream, mut req: Request) -> Result<()> { let reader = BufReader::new(&stream); let mut lines = reader.lines(); if let Some(Ok(line)) = lines.next() { @@ -75,15 +75,24 @@ fn accept(stream: TcpStream, mut req: Request) -> Result<()> { color::Reset ); req.parse_request(&line); - write_response(&stream, req)?; + write_response(&mut stream, req)?; } Ok(()) } +/// Render a response to a String. +pub fn render(host: &str, port: u16, root: &str, selector: &str) -> Result { + let mut req = Request::from(host, port, root)?; + req.parse_request(&selector); + let mut out = vec![]; + write_response(&mut out, req)?; + Ok(String::from_utf8_lossy(&out).into()) +} + /// Writes a response to a client based on a Request. -fn write_response<'a, W>(w: &'a W, mut req: Request) -> Result<()> +fn write_response(w: &mut W, mut req: Request) -> Result<()> where - &'a W: Write, + W: Write, { let path = req.file_path(); @@ -121,9 +130,9 @@ where } /// Send a directory listing (menu) to the client based on a Request. -fn write_dir<'a, W>(w: &'a W, req: Request) -> Result<()> +fn write_dir(w: &mut W, req: Request) -> Result<()> where - &'a W: Write, + W: Write, { let path = req.file_path(); if !fs_exists(&path) { @@ -144,7 +153,6 @@ where )?; } - let mut menu = GopherMenu::with_write(w); let rel_path = req.relative_file_path(); // show directory entries @@ -161,8 +169,10 @@ where rel_path.trim_end_matches('/'), file_name.to_string_lossy() ); - menu.write_entry( - file_type(&entry), + write!( + w, + "{}{}\t{}\t{}\t{}\r\n", + file_type(&entry).to_char(), &file_name.to_string_lossy(), &path, &req.host, @@ -182,7 +192,8 @@ where )?; } - menu.end()?; + write!(w, ".\r\n"); + println!( "{}│{} Server reply:\t{}DIR {}{}{}", color::Green, @@ -196,13 +207,13 @@ where } /// Send a file to the client based on a Request. -fn write_file<'a, W>(mut w: &'a W, req: Request) -> Result<()> +fn write_file(w: &mut W, req: Request) -> Result<()> where - &'a W: Write, + W: Write, { let path = req.file_path(); let mut f = fs::File::open(&path)?; - io::copy(&mut f, &mut w)?; + io::copy(&mut f, w)?; println!( "{}│{} Server reply:\t{}FILE {}{}{}", color::Green, @@ -216,9 +227,9 @@ where } /// Send a gophermap (menu) to the client based on a Request. -fn write_gophermap<'a, W>(mut w: &'a W, req: Request) -> Result<()> +fn write_gophermap(w: &mut W, req: Request) -> Result<()> where - &'a W: Write, + W: Write, { let path = req.file_path(); @@ -230,7 +241,7 @@ where }; for line in reader.lines() { - w.write_all(gph_line_to_gopher(line, &req).as_bytes())?; + write!(w, "{}", gph_line_to_gopher(line, &req))?; } println!( "{}│{} Server reply:\t{}MAP {}{}{}", @@ -298,9 +309,9 @@ fn gph_line_to_gopher(line: &str, req: &Request) -> String { line } -fn write_not_found<'a, W>(mut w: &'a W, req: Request) -> Result<()> +fn write_not_found(w: &mut W, req: Request) -> Result<()> where - &'a W: Write, + W: Write, { let line = format!("3Not Found: {}\t/\tnone\t70\r\n", req.selector); println!( @@ -310,7 +321,7 @@ where req.relative_file_path(), color::Reset, ); - w.write_all(line.as_bytes())?; + write!(w, "{}", line)?; Ok(()) }