commit - d04a1dedc45b2aabb2ec9bae57604939a196b9be
commit + b38936bbf89f342bc53320ba9f16f0854cdfbc63
blob - a636ec040dd42996f384f3d9894e6b126d0f2f53
blob + 146194c4142774a611adc3ce8fbff389c45ad55b
--- wp.rs
+++ wp.rs
path::{Path, PathBuf},
};
+#[cfg(target_os = "openbsd")]
+mod sandbox {
+ use std::ffi::CString;
+ use std::os::raw::c_char;
+ use std::path::Path;
+
+ extern "C" {
+ fn pledge(promises: *const c_char, execpromises: *const c_char) -> i32;
+ fn unveil(path: *const c_char, permissions: *const c_char) -> i32;
+ }
+
+ pub fn do_unveil(path: &Path, perms: &str) {
+ let p = CString::new(path.to_str().expect("unveil: invalid path"))
+ .expect("unveil: nul in path");
+ let f = CString::new(perms).expect("unveil: nul in perms");
+ if unsafe { unveil(p.as_ptr(), f.as_ptr()) } == -1 {
+ let e = std::io::Error::last_os_error();
+ super::err(&format!("unveil {}: {}", path.display(), e));
+ }
+ }
+
+ pub fn lock_unveil() {
+ if unsafe { unveil(std::ptr::null(), std::ptr::null()) } == -1 {
+ let e = std::io::Error::last_os_error();
+ super::err(&format!("unveil lock: {}", e));
+ }
+ }
+
+ pub fn do_pledge(promises: &str) {
+ let p = CString::new(promises).expect("pledge: nul in promises");
+ if unsafe { pledge(p.as_ptr(), std::ptr::null()) } == -1 {
+ let e = std::io::Error::last_os_error();
+ super::err(&format!("pledge: {}", e));
+ }
+ }
+}
+
const VERSION: &str = "0.1.0";
const NAME: &str = "wp";
const FAVICON: &[u8] = include_bytes!("assets/favicon.ico");
let modules = vars.get("MODULES").cloned().unwrap_or_default();
for module in modules.split_whitespace() {
+ if module.contains("..") {
+ continue;
+ }
load_module(module, portsdir, cache);
if let Some(modvars) = cache.get(module) {
for (k, v) in modvars {
Some(vars)
}
+fn safe_name(s: &str) -> bool {
+ !s.is_empty()
+ && !s.contains('/')
+ && !s.contains('\0')
+ && s != "."
+ && s != ".."
+}
+
fn port_from_makefile(
category: &str,
portname: &str,
portsdir: &Path,
modcache: &mut ModCache,
) -> Option<Port> {
+ if !safe_name(category) || !safe_name(portname) {
+ return None;
+ }
let mf = portdir.join("Makefile");
let mut vars = parse_makefile(&mf)?;
portsdir: Option<&Path>,
) -> Option<Port> {
let (category, name) = path.split_once('/')?;
+ if !safe_name(category) || !safe_name(name) {
+ return None;
+ }
let comment = vars.get("COMMENT").filter(|s| !s.is_empty())?;
let descr = if let Some(pd) = portsdir {
for line in text.lines() {
// format: category/port.VARIABLE=value
- let Some(dot) = line.find('.') else {
+ // find the first '=' then look backwards for the '.'
+ let Some(eq_pos) = line.find('=') else {
continue;
};
+ let prefix = &line[..eq_pos];
+ let Some(dot) = prefix.rfind('.') else {
+ continue;
+ };
let portpath = &line[..dot];
- let rest = &line[dot + 1..];
- let Some(eq) = rest.find('=') else {
- continue;
- };
- let var = &rest[..eq];
- let val = &rest[eq + 1..];
+ let var = &line[dot + 1..eq_pos];
+ let val = &line[eq_pos + 1..];
by_port
.entry(portpath.to_string())
.or_default()
}
fn html_escape(s: &str) -> String {
- if !s.contains(&['&', '<', '>', '"'][..]) {
+ if !s.contains(&['&', '<', '>', '"', '\''][..]) {
return s.to_string();
}
let mut out = String::with_capacity(s.len() + 8);
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
+ '\'' => out.push_str("'"),
_ => out.push(c),
}
}
));
}
- if !p.homepage.is_empty() {
+ if !p.homepage.is_empty()
+ && (p.homepage.starts_with("https://")
+ || p.homepage.starts_with("http://"))
+ {
html.push_str(&format!(
"<p>Homepage: <a href=\"{}\">{}</a></p>\n",
html_escape(&p.homepage),
}
}
+fn sanitize_field(s: &str) -> String {
+ s.replace(['\t', '\n'], " ").replace('\r', "")
+}
+
fn write_index_txt(ports: &[Port], outdir: &Path) {
let mut out = String::new();
for p in ports {
out.push('/');
out.push_str(&p.name);
out.push('\t');
- out.push_str(&p.pkgname);
+ out.push_str(&sanitize_field(&p.pkgname));
out.push('\t');
- out.push_str(&p.comment);
+ out.push_str(&sanitize_field(&p.comment));
out.push('\t');
- out.push_str(&p.maintainer);
+ out.push_str(&sanitize_field(&p.maintainer));
out.push('\n');
}
write_file(&outdir.join("INDEX.txt"), &out);
let hex = [hi, lo];
if let Ok(s) = std::str::from_utf8(&hex) {
if let Ok(n) = u8::from_str_radix(s, 16) {
- out.push(n);
+ if n != 0 {
+ out.push(n);
+ }
continue;
}
}
}
fn cgi_search(index_path: &Path) {
- let qs = std::env::var("QUERY_STRING").unwrap_or_default();
- let query = qs
+ let mut qs = std::env::var("QUERY_STRING").unwrap_or_default();
+ qs.truncate(1024);
+ let mut query = qs
.split('&')
.find_map(|p| p.strip_prefix("q=").map(url_decode))
.unwrap_or_default()
.to_lowercase();
+ query.truncate(128);
print!("Content-Type: text/html\r\n\r\n");
None => continue,
};
+ if count >= 200 {
+ break;
+ }
html.push_str(&format!(
"<tr><td><a href=\"/ports/{}/{}.html\">\
{}</a></td><td>{}</td><td>{}</td></tr>\n",
}
html.push_str("</table>\n");
+ if count >= 200 {
+ html.push_str("<p>Results limited to 200.</p>\n");
+ }
html.push_str(&format!(
"<p>{} results for \"{}\"</p>\n",
count,
// CGI mode: QUERY_STRING is set by the HTTP server
if std::env::var("QUERY_STRING").is_ok() {
let index = PathBuf::from("INDEX.txt");
+ #[cfg(target_os = "openbsd")]
+ {
+ sandbox::do_unveil(&index, "r");
+ sandbox::lock_unveil();
+ sandbox::do_pledge("stdio rpath");
+ }
cgi_search(&index);
return;
}
i += 1;
}
+ fs::create_dir_all(&outdir).unwrap_or_else(|e| {
+ errio(&outdir, &e);
+ });
+
+ #[cfg(target_os = "openbsd")]
+ {
+ sandbox::do_unveil(&portsdir, "r");
+ if let Some(ref d) = dumpfile {
+ sandbox::do_unveil(d, "r");
+ }
+ sandbox::do_unveil(&outdir, "rwc");
+ sandbox::lock_unveil();
+ sandbox::do_pledge("stdio rpath wpath cpath");
+ }
+
let ports = if let Some(ref dump) = dumpfile {
eprintln!("loading dump from {}", dump.display());
let pd = if portsdir.is_dir() {