~zethra/stargazer

69fcbf0cdae4b3e9ddcb5f3ff924f9c53a155894 — Ben Aaron Goldberg 14 days ago a24e8cf
server: added lang and charset params for static files

This allows users to specify a lang or charset param for requests to a
static route.
M doc/stargazer-ini.scd => doc/stargazer-ini.scd +19 -0
@@ 137,6 137,25 @@ Within each routing section, the following keys are used to configure how
	list of files in the requested directory when an index file cannot be
	found. Off by default. Mutually exclusive with *cgi*.

*lang*
	Set this value as the *lang* parameter for gemini files served under this
	route. The lang param will only be set for text/gemini responses. The Gemini
	Specification says that the lang parameter should contain a comma separated
	list of language identifier from RFC 4646. Stargazer will use the lang
	parameter as given in this config. It is up to the user to provid a valid
	list of languages. Currently, this parameter is only used when serving
	static files.

*charset*
	Set this value as the *charset* parameter for text files served under this
	route. The lang param will only be set for text/\* responses. If the lang
	paraparameter is also set, then lang will be set instead of charset for
	text/gemini responses. The Gemini Specification says that the charset
	parameter should be a character set specified in RFC 2046. Stargazer will
	use the charset parameter as given in this config. It is up to the user to
	provid a valid charset value. Currently, this parameter is only used when
	serving static files.

*redirect*
	Send a redirect to this URI instead of serving other content. The URI
	exactly as written will be send to the client. Mutually exclusive with most

M scripts/gemini-diagnostics => scripts/gemini-diagnostics +26 -0
@@ 817,6 817,32 @@ class RedirectRoute(BaseCheck):
        log("Meta should be ..")
        self.log_test(f"{response.meta!r}", response.meta == "..")

class LangParam(BaseCheck):
    """Check that the lang param is included in the MIME type"""

    def check(self):
        url = "gemini://localhost/en.gmi\r\n"
        response = self.make_request(url)

        log("Status should return code 20")
        self.log_test(f"{response.status!r}", response.status == "20")

        log("Meta should be text/gemini; lang=en")
        self.log_test(f"{response.meta!r}", response.meta == "text/gemini; lang=en")
        
class CharsetParam(BaseCheck):
    """Check that the charset param is included in the MIME type"""

    def check(self):
        url = "gemini://localhost/plain.txt\r\n"
        response = self.make_request(url)

        log("Status should return code 20")
        self.log_test(f"{response.status!r}", response.status == "20")

        log("Meta should be text/plain; charset=ascii")
        self.log_test(f"{response.meta!r}", response.meta == "text/plain; charset=ascii")

# noinspection PyTypeChecker
# fmt: off
parser = argparse.ArgumentParser(

M src/config.rs => src/config.rs +5 -2
@@ 238,12 238,13 @@ pub fn load(config_path: impl AsRef<Path>) -> Result<Config> {
                }
            };

            let rewrite = props.remove("rewrite");
            sites.push(Route {
                domain,
                path: route,
                rewrite,
                rewrite: props.remove("rewrite"),
                route_type,
                lang: props.remove("lang"),
                charset: props.remove("charset"),
            });
            check_section_empty(section, props)?;
        }


@@ 285,6 286,8 @@ pub fn dev_config() -> Result<Config> {
            domain: "localhost".to_owned(),
            path: RoutePath::All,
            rewrite: None,
            lang: None,
            charset: None,
            route_type: StaticRoute {
                root,
                index: "index.gmi".to_owned(),

M src/get_file.rs => src/get_file.rs +22 -2
@@ 38,13 38,16 @@ enum PathType {
pub async fn get_file<'a>(
    static_route: &'static StaticRoute,
    path: &'a str,
    lang: &'a Option<String>,
    charset: &'a Option<String>,
    stream: &'a mut TlsStream<TcpStream>,
) -> Result<()> {
    let path_str = path;
    let os_str = parse_path(path)?;
    let path = static_route.root.join(Path::new(&os_str));
    log::debug!("Requested file path: {}", path.display());

    let (path, path_type, mime_type) = unblock(move || {
    let (path, path_type, mut mime_type) = unblock(move || {
        if !path.exists() {
            return Err(GemError::NotFound);
        }


@@ 66,19 69,36 @@ pub async fn get_file<'a>(
    if path_type == PathType::Dir && !static_route.auto_index {
        return Err(GemError::NotFound);
    }
    // TODO show I be doing this?
    if path_type == PathType::Dir && !path_str.ends_with('/') {
        let mut new_path = path_str.to_owned();
        new_path.push('/');
        return Err(GemError::Redirect(new_path));
    }

    // Add lang or charset to mime_type
    match (lang, charset) {
        (Some(lang), _) if mime_type == GMI_MIME => {
            mime_type.push_str("; lang=");
            mime_type.push_str(&lang);
        }
        (_, Some(charset)) if mime_type.starts_with("text/") => {
            mime_type.push_str("; charset=");
            mime_type.push_str(&charset);
        }
        _ => {}
    }

    let mut buf = [0u8; 4096];
    let mut file = File::open(&path)
        .await
        .with_context(|| format!("Error opening file: {}", path.display()))?;

    stream.write_all(b"20 ").await.into_io_error()?;
    stream.write_all(mime_type.as_bytes()).await.into_io_error()?;
    stream
        .write_all(mime_type.as_bytes())
        .await
        .into_io_error()?;
    stream.write_all(b"\r\n").await.into_io_error()?;
    match path_type {
        PathType::File => loop {

M src/main.rs => src/main.rs +9 -2
@@ 202,8 202,8 @@ fn main() {

#[cfg(unix)]
async fn exit_on_sig() -> Result<()> {
    use signal_hook_async_std::Signals;
    use signal_hook::consts::{SIGINT, SIGTERM};
    use signal_hook_async_std::Signals;

    let mut signals = Signals::new(&[SIGTERM, SIGINT])?;
    signals.next().await;


@@ 429,7 429,14 @@ async fn handle_connection(
            stream.write_all(b"\r\n").await.into_io_error()?;
        }
        RouteType::Static(static_route) => {
            get_file(static_route, &req.path, stream).await?;
            get_file(
                static_route,
                &req.path,
                &route.lang,
                &route.charset,
                stream,
            )
            .await?;
        }
    }


M src/router.rs => src/router.rs +2 -0
@@ 31,6 31,8 @@ pub struct Route {
    pub path: RoutePath,
    /// Rewrite rule
    pub rewrite: Option<String>,
    pub lang: Option<String>,
    pub charset: Option<String>,
    pub route_type: RouteType,
}


A test_data/test_site/en.gmi => test_data/test_site/en.gmi +1 -0
@@ 0,0 1,1 @@
# This Page is english

A test_data/test_site/plain.txt => test_data/test_site/plain.txt +1 -0
@@ 0,0 1,1 @@
Test

M test_data/testing.ini => test_data/testing.ini +10 -0
@@ 7,6 7,16 @@ store = ./test_data/store
root = ./test_data/test_site
auto-index = on

[localhost=/en.gmi]
root = ./test_data/test_site
lang = en
charset = ascii

[localhost=/plain.txt]
root = ./test_data/test_site
lang = en
charset = ascii

[localhost:/cgi-bin]
root = ./test_data
cgi = on