Redirects these to the cgit commit view. Only supports cgit because we don't have a good way to coax Sourcegraph into fetching these refs. Change-Id: I8c28ed015ba37c04eb4b7a667bde70ff6a92bf4c Reviewed-on: https://cl.tvl.fyi/c/depot/+/3772 Tested-by: BuildkiteCI Reviewed-by: sterni <sternenseemann@systemli.org>
		
			
				
	
	
		
			388 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			388 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
//! Atward implements TVL's redirection service, living at
 | 
						|
//! atward.tvl.fyi
 | 
						|
//!
 | 
						|
//! This service is designed to be added as a search engine to web
 | 
						|
//! browsers and attempts to send users to useful locations based on
 | 
						|
//! their search query (falling back to another search engine).
 | 
						|
use regex::Regex;
 | 
						|
use rouille::input::cookies;
 | 
						|
use rouille::{Request, Response};
 | 
						|
 | 
						|
/// A query handler supported by atward. It consists of a pattern on
 | 
						|
/// which to match and trigger the query, and a function to execute
 | 
						|
/// that returns the target URL.
 | 
						|
struct Handler {
 | 
						|
    /// Regular expression on which to match the query string.
 | 
						|
    pattern: Regex,
 | 
						|
 | 
						|
    /// Function to construct the target URL. If the pattern matches,
 | 
						|
    /// this is invoked with the captured matches and the entire URI.
 | 
						|
    ///
 | 
						|
    /// Returning `None` causes atward to fall through to the next
 | 
						|
    /// query (and eventually to the default search engine).
 | 
						|
    target: for<'s> fn(&Query, regex::Captures<'s>) -> Option<String>,
 | 
						|
}
 | 
						|
 | 
						|
/// An Atward query supplied by a user.
 | 
						|
#[derive(Debug, PartialEq)]
 | 
						|
struct Query {
 | 
						|
    /// Query string itself.
 | 
						|
    query: String,
 | 
						|
 | 
						|
    /// Should Sourcegraph be used instead of cgit?
 | 
						|
    cs: bool,
 | 
						|
}
 | 
						|
 | 
						|
/// Helper function for setting a parameter based on a query
 | 
						|
/// parameter.
 | 
						|
fn query_setting(req: &Request, config: &mut bool, param: &str) {
 | 
						|
    match req.get_param(param) {
 | 
						|
        Some(s) if s == "true" => *config = true,
 | 
						|
        Some(s) if s == "false" => *config = false,
 | 
						|
        _ => {}
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
impl Query {
 | 
						|
    fn from_request(req: &Request) -> Option<Query> {
 | 
						|
        // First extract the actual search query ...
 | 
						|
        let mut query = match req.get_param("q") {
 | 
						|
            Some(query) => Query { query, cs: false },
 | 
						|
            None => return None,
 | 
						|
        };
 | 
						|
 | 
						|
        // ... then apply settings to it. Settings in query parameters
 | 
						|
        // take precedence over cookies.
 | 
						|
        for cookie in cookies(req) {
 | 
						|
            match cookie {
 | 
						|
                ("cs", "true") => {
 | 
						|
                    query.cs = true;
 | 
						|
                }
 | 
						|
                _ => {}
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        query_setting(req, &mut query.cs, "cs");
 | 
						|
 | 
						|
        Some(query)
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
#[cfg(test)]
 | 
						|
impl From<&str> for Query {
 | 
						|
    fn from(query: &str) -> Query {
 | 
						|
        Query {
 | 
						|
            query: query.to_string(),
 | 
						|
            cs: false,
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/// Create a URL to a file (and, optionally, specific line) in cgit.
 | 
						|
fn cgit_url(path: &str) -> String {
 | 
						|
    if path.ends_with(".md") {
 | 
						|
        format!("https://code.tvl.fyi/about/{}", path)
 | 
						|
    } else {
 | 
						|
        format!("https://code.tvl.fyi/tree/{}", path)
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/// Create a URL to a path in Sourcegraph.
 | 
						|
fn sourcegraph_path_url(path: &str) -> String {
 | 
						|
    format!("https://cs.tvl.fyi/depot/-/tree/{}", path)
 | 
						|
}
 | 
						|
/// Definition of all supported query handlers in atward.
 | 
						|
fn handlers() -> Vec<Handler> {
 | 
						|
    vec![
 | 
						|
        // Bug IDs (e.g. b/123)
 | 
						|
        Handler {
 | 
						|
            pattern: Regex::new("^b/(?P<bug>\\d+)$").unwrap(),
 | 
						|
            target: |_, captures| Some(format!("https://b.tvl.fyi/{}", &captures["bug"])),
 | 
						|
        },
 | 
						|
        // Changelists (e.g. cl/42)
 | 
						|
        Handler {
 | 
						|
            pattern: Regex::new("^cl/(?P<cl>\\d+)$").unwrap(),
 | 
						|
            target: |_, captures| Some(format!("https://cl.tvl.fyi/{}", &captures["cl"])),
 | 
						|
        },
 | 
						|
        // Non-parameterised short hostnames should redirect to $host.tvl.fyi
 | 
						|
        Handler {
 | 
						|
            pattern: Regex::new("^(?P<host>b|cl|cs|code|at|todo)$").unwrap(),
 | 
						|
            target: |_, captures| Some(format!("https://{}.tvl.fyi/", &captures["host"])),
 | 
						|
        },
 | 
						|
        // Depot revisions (e.g. r/3002)
 | 
						|
        Handler {
 | 
						|
            pattern: Regex::new("^r/(?P<rev>\\d+)$").unwrap(),
 | 
						|
            target: |_, captures| {
 | 
						|
                Some(format!(
 | 
						|
                    "https://code.tvl.fyi/commit/?id=refs/r/{}",
 | 
						|
                    &captures["rev"]
 | 
						|
                ))
 | 
						|
            },
 | 
						|
        },
 | 
						|
        // Depot paths (e.g. //web/atward or //ops/nixos/whitby/default.nix)
 | 
						|
        // TODO(tazjin): Add support for specifying lines in a query parameter
 | 
						|
        Handler {
 | 
						|
            pattern: Regex::new("^//(?P<path>[a-zA-Z].*)?$").unwrap(),
 | 
						|
            target: |query, captures| {
 | 
						|
                // Pass an empty string if the path is missing, to
 | 
						|
                // redirect to the depot root.
 | 
						|
                let path = captures.name("path").map(|m| m.as_str()).unwrap_or("");
 | 
						|
 | 
						|
                if query.cs {
 | 
						|
                    Some(sourcegraph_path_url(path))
 | 
						|
                } else {
 | 
						|
                    Some(cgit_url(path))
 | 
						|
                }
 | 
						|
            },
 | 
						|
        },
 | 
						|
    ]
 | 
						|
}
 | 
						|
 | 
						|
/// Attempt to match against all known query types, and return the
 | 
						|
/// destination URL if one is found.
 | 
						|
fn dispatch(handlers: &[Handler], query: &Query) -> Option<String> {
 | 
						|
    for handler in handlers {
 | 
						|
        if let Some(captures) = handler.pattern.captures(&query.query) {
 | 
						|
            if let Some(destination) = (handler.target)(query, captures) {
 | 
						|
                return Some(destination);
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    None
 | 
						|
}
 | 
						|
 | 
						|
/// Return the opensearch.xml file which is required for adding atward
 | 
						|
/// as a search engine in Firefox.
 | 
						|
fn opensearch() -> Response {
 | 
						|
    Response::text(include_str!("opensearch.xml"))
 | 
						|
        .with_unique_header("Content-Type", "application/opensearchdescription+xml")
 | 
						|
}
 | 
						|
 | 
						|
/// Render the atward index page which gives users some information
 | 
						|
/// about how to use the service.
 | 
						|
fn index() -> Response {
 | 
						|
    Response::html(include_str!(env!("ATWARD_INDEX_HTML")))
 | 
						|
}
 | 
						|
 | 
						|
/// Render the fallback page which informs users that their query is
 | 
						|
/// unsupported.
 | 
						|
fn fallback() -> Response {
 | 
						|
    Response::text("error for emphasis that i am angery and the query whimchst i angery atward")
 | 
						|
        .with_status_code(404)
 | 
						|
}
 | 
						|
 | 
						|
fn main() {
 | 
						|
    let queries = handlers();
 | 
						|
    let address = std::env::var("ATWARD_LISTEN_ADDRESS")
 | 
						|
        .expect("ATWARD_LISTEN_ADDRESS environment variable must be set");
 | 
						|
 | 
						|
    rouille::start_server(&address, move |request| {
 | 
						|
        rouille::log(&request, std::io::stderr(), || {
 | 
						|
            if request.url() == "/opensearch.xml" {
 | 
						|
                return opensearch();
 | 
						|
            }
 | 
						|
 | 
						|
            let query = match Query::from_request(&request) {
 | 
						|
                Some(q) => q,
 | 
						|
                None => return index(),
 | 
						|
            };
 | 
						|
 | 
						|
            match dispatch(&queries, &query) {
 | 
						|
                None => fallback(),
 | 
						|
                Some(destination) => Response::redirect_303(destination),
 | 
						|
            }
 | 
						|
        })
 | 
						|
    });
 | 
						|
}
 | 
						|
 | 
						|
#[cfg(test)]
 | 
						|
mod tests {
 | 
						|
    use super::*;
 | 
						|
 | 
						|
    #[test]
 | 
						|
    fn bug_query() {
 | 
						|
        assert_eq!(
 | 
						|
            dispatch(&handlers(), &"b/42".into()),
 | 
						|
            Some("https://b.tvl.fyi/42".to_string())
 | 
						|
        );
 | 
						|
 | 
						|
        assert_eq!(
 | 
						|
            dispatch(&handlers(), &"something only mentioning b/42".into()),
 | 
						|
            None,
 | 
						|
        );
 | 
						|
        assert_eq!(dispatch(&handlers(), &"b/invalid".into()), None,);
 | 
						|
    }
 | 
						|
 | 
						|
    #[test]
 | 
						|
    fn cl_query() {
 | 
						|
        assert_eq!(
 | 
						|
            dispatch(&handlers(), &"cl/42".into()),
 | 
						|
            Some("https://cl.tvl.fyi/42".to_string())
 | 
						|
        );
 | 
						|
 | 
						|
        assert_eq!(
 | 
						|
            dispatch(&handlers(), &"something only mentioning cl/42".into()),
 | 
						|
            None,
 | 
						|
        );
 | 
						|
        assert_eq!(dispatch(&handlers(), &"cl/invalid".into()), None,);
 | 
						|
    }
 | 
						|
 | 
						|
    #[test]
 | 
						|
    fn depot_path_cgit_query() {
 | 
						|
        assert_eq!(
 | 
						|
            dispatch(&handlers(), &"//web/atward/default.nix".into()),
 | 
						|
            Some("https://code.tvl.fyi/tree/web/atward/default.nix".to_string()),
 | 
						|
        );
 | 
						|
 | 
						|
        assert_eq!(
 | 
						|
            dispatch(&handlers(), &"//nix/readTree/README.md".into()),
 | 
						|
            Some("https://code.tvl.fyi/about/nix/readTree/README.md".to_string()),
 | 
						|
        );
 | 
						|
 | 
						|
        assert_eq!(dispatch(&handlers(), &"/not/a/depot/path".into()), None);
 | 
						|
    }
 | 
						|
 | 
						|
    #[test]
 | 
						|
    fn depot_path_sourcegraph_query() {
 | 
						|
        assert_eq!(
 | 
						|
            dispatch(
 | 
						|
                &handlers(),
 | 
						|
                &Query {
 | 
						|
                    query: "//web/atward/default.nix".to_string(),
 | 
						|
                    cs: true,
 | 
						|
                }
 | 
						|
            ),
 | 
						|
            Some("https://cs.tvl.fyi/depot/-/tree/web/atward/default.nix".to_string()),
 | 
						|
        );
 | 
						|
 | 
						|
        assert_eq!(
 | 
						|
            dispatch(
 | 
						|
                &handlers(),
 | 
						|
                &Query {
 | 
						|
                    query: "/not/a/depot/path".to_string(),
 | 
						|
                    cs: true,
 | 
						|
                }
 | 
						|
            ),
 | 
						|
            None
 | 
						|
        );
 | 
						|
    }
 | 
						|
 | 
						|
    #[test]
 | 
						|
    fn depot_root_cgit_query() {
 | 
						|
        assert_eq!(
 | 
						|
            dispatch(
 | 
						|
                &handlers(),
 | 
						|
                &Query {
 | 
						|
                    query: "//".to_string(),
 | 
						|
                    cs: false,
 | 
						|
                }
 | 
						|
            ),
 | 
						|
            Some("https://code.tvl.fyi/tree/".to_string()),
 | 
						|
        );
 | 
						|
    }
 | 
						|
 | 
						|
    #[test]
 | 
						|
    fn plain_host_queries() {
 | 
						|
        assert_eq!(
 | 
						|
            dispatch(&handlers(), &"cs".into()),
 | 
						|
            Some("https://cs.tvl.fyi/".to_string()),
 | 
						|
        );
 | 
						|
 | 
						|
        assert_eq!(
 | 
						|
            dispatch(&handlers(), &"cl".into()),
 | 
						|
            Some("https://cl.tvl.fyi/".to_string()),
 | 
						|
        );
 | 
						|
 | 
						|
        assert_eq!(
 | 
						|
            dispatch(&handlers(), &"b".into()),
 | 
						|
            Some("https://b.tvl.fyi/".to_string()),
 | 
						|
        );
 | 
						|
 | 
						|
        assert_eq!(
 | 
						|
            dispatch(&handlers(), &"todo".into()),
 | 
						|
            Some("https://todo.tvl.fyi/".to_string()),
 | 
						|
        );
 | 
						|
    }
 | 
						|
 | 
						|
    #[test]
 | 
						|
    fn request_to_query() {
 | 
						|
        assert_eq!(
 | 
						|
            Query::from_request(&Request::fake_http("GET", "/?q=b%2F42", vec![], vec![]))
 | 
						|
                .expect("request should parse to a query"),
 | 
						|
            Query {
 | 
						|
                query: "b/42".to_string(),
 | 
						|
                cs: false,
 | 
						|
            },
 | 
						|
        );
 | 
						|
 | 
						|
        assert_eq!(
 | 
						|
            Query::from_request(&Request::fake_http("GET", "/", vec![], vec![])),
 | 
						|
            None
 | 
						|
        );
 | 
						|
    }
 | 
						|
 | 
						|
    #[test]
 | 
						|
    fn settings_from_cookie() {
 | 
						|
        assert_eq!(
 | 
						|
            Query::from_request(&Request::fake_http(
 | 
						|
                "GET",
 | 
						|
                "/?q=b%2F42",
 | 
						|
                vec![("Cookie".to_string(), "cs=true;".to_string())],
 | 
						|
                vec![]
 | 
						|
            ))
 | 
						|
            .expect("request should parse to a query"),
 | 
						|
            Query {
 | 
						|
                query: "b/42".to_string(),
 | 
						|
                cs: true,
 | 
						|
            },
 | 
						|
        );
 | 
						|
    }
 | 
						|
 | 
						|
    #[test]
 | 
						|
    fn settings_from_query_parameter() {
 | 
						|
        assert_eq!(
 | 
						|
            Query::from_request(&Request::fake_http(
 | 
						|
                "GET",
 | 
						|
                "/?q=b%2F42&cs=true",
 | 
						|
                vec![],
 | 
						|
                vec![]
 | 
						|
            ))
 | 
						|
            .expect("request should parse to a query"),
 | 
						|
            Query {
 | 
						|
                query: "b/42".to_string(),
 | 
						|
                cs: true,
 | 
						|
            },
 | 
						|
        );
 | 
						|
 | 
						|
        // Query parameter should override cookie
 | 
						|
        assert_eq!(
 | 
						|
            Query::from_request(&Request::fake_http(
 | 
						|
                "GET",
 | 
						|
                "/?q=b%2F42&cs=false",
 | 
						|
                vec![("Cookie".to_string(), "cs=true;".to_string())],
 | 
						|
                vec![]
 | 
						|
            ))
 | 
						|
            .expect("request should parse to a query"),
 | 
						|
            Query {
 | 
						|
                query: "b/42".to_string(),
 | 
						|
                cs: false,
 | 
						|
            },
 | 
						|
        );
 | 
						|
    }
 | 
						|
 | 
						|
    #[test]
 | 
						|
    fn depot_revision_query() {
 | 
						|
        assert_eq!(
 | 
						|
            dispatch(&handlers(), &"r/3002".into()),
 | 
						|
            Some("https://code.tvl.fyi/commit/?id=refs/r/3002".to_string())
 | 
						|
        );
 | 
						|
 | 
						|
        assert_eq!(
 | 
						|
            dispatch(&handlers(), &"something only mentioning r/3002".into()),
 | 
						|
            None,
 | 
						|
        );
 | 
						|
 | 
						|
        assert_eq!(dispatch(&handlers(), &"r/invalid".into()), None,);
 | 
						|
    }
 | 
						|
}
 |