1use qualname::{NamespaceUri, NcName, QName};
6use std::fs;
7use std::path::Path;
8use std::rc::Rc;
9use std::sync::LazyLock;
10use tokio::runtime::Runtime;
11use url::Url;
12use xrust::item::{Item, Node};
13use xrust::security::{Policy, SecurityResult};
14use xrust::transform::Transform;
15use xrust::transform::callable::ActualParameters;
16use xrust::value::Value;
17use xrust::xdmerror::{Error, ErrorKind};
18
19pub fn resolve<N: Node>(url: &Url, policy: &Rc<Policy<N>>) -> Result<String, Error> {
43 let rt = Runtime::new().unwrap();
44 rt.block_on(resolve_internal(url, policy))
45}
46async fn resolve_internal<N: Node>(url: &Url, policy: &Rc<Policy<N>>) -> Result<String, Error> {
47 if policy.get(
49 &*ACCESS_QNAME,
50 ActualParameters::Named(vec![
51 (
52 PARAM_URL.clone(),
53 Transform::Literal(Item::Value(Rc::new(Value::from(url.to_string())))),
54 ), ]),
56 )? == SecurityResult::NotPermitted
57 {
58 return Err(Error::new(
59 ErrorKind::NotPermitted,
60 "resolving URL not permitted by security policy",
61 ));
62 }
63
64 let limit = match policy.get(
66 &*MAXSIZE_QNAME,
67 ActualParameters::Named(vec![
68 (
69 PARAM_URL.clone(),
70 Transform::Literal(Item::Value(Rc::new(Value::from(url.to_string())))),
71 ), ]),
73 )? {
74 SecurityResult::NotPermitted => {
75 return Err(Error::new(
76 ErrorKind::NotPermitted,
77 "access to URL not permitted by security policy",
78 ));
79 }
80 SecurityResult::Permitted(None) => None,
81 SecurityResult::Permitted(Some(v)) => {
82 if let Ok(i) = v.parse::<usize>() {
83 Some(i)
84 } else {
85 return Err(Error::new(
86 ErrorKind::NotPermitted,
87 "security policy permits invalid value",
88 ));
89 }
90 }
91 };
92
93 match url.scheme() {
97 "http" => {
98 let mut body = String::new();
99 let mut res = reqwest::get(url.to_string())
100 .await
101 .map_err(|e| Error::new(ErrorKind::Unknown, e.to_string()))?;
102 while let Some(chunk) = res
103 .chunk()
104 .await
105 .map_err(|e| Error::new(ErrorKind::Unknown, e.to_string()))?
106 {
107 if limit.is_some_and(|v| body.len() + chunk.len() > v) {
108 return Err(Error::new(
109 ErrorKind::NotPermitted,
110 format!("resource exceeds allowed length \"{}\"", limit.unwrap()),
111 ));
112 } else {
113 let frag = chunk.utf8_chunks().try_fold(String::new(), |mut acc, c| {
114 acc.push_str(c.valid());
115 if !c.invalid().is_empty() {
116 Err(Error::new(ErrorKind::ParseError, "invalid UTF-8 character"))
117 } else {
118 Ok(acc)
119 }
120 })?;
121 body.push_str(frag.as_str())
122 }
123 }
124 Ok(body)
125 }
126 "file" => {
127 let body = fs::read_to_string(Path::new(url.path()))
128 .map_err(|er| Error::new(ErrorKind::Unknown, er.to_string()))?;
129 if limit.is_some_and(|v| body.len() > v) {
130 Err(Error::new(
131 ErrorKind::NotPermitted,
132 format!("resource exceeds allowed length \"{}\"", limit.unwrap()),
133 ))
134 } else {
135 Ok(body)
136 }
137 }
138 _ => Err(Error::new(
139 ErrorKind::Unknown,
140 format!("unable to fetch URL \"{}\"", url.to_string()),
141 )),
142 }
143}
144
145static SECURITYNS: LazyLock<Option<NamespaceUri>> = LazyLock::new(|| {
147 Some(
148 NamespaceUri::try_from("http://gitlab.gnome.org/World/Rust/markup-rs/xrust-net/security")
149 .unwrap(),
150 )
151});
152static ACCESS_QNAME: LazyLock<QName> = LazyLock::new(|| {
153 QName::new_from_parts(NcName::try_from("access").unwrap(), SECURITYNS.clone())
154});
155static MAXSIZE_QNAME: LazyLock<QName> = LazyLock::new(|| {
156 QName::new_from_parts(
157 NcName::try_from("maximum-size").unwrap(),
158 SECURITYNS.clone(),
159 )
160});
161static PARAM_URL: LazyLock<QName> =
162 LazyLock::new(|| QName::new_from_parts(NcName::try_from("url").unwrap(), None));
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 use xrust::item::{Item, Node, SequenceTrait};
169 use xrust::parser::ParseError;
170 use xrust::parser::xml::parse as xmlparse;
171 use xrust::security::{Feature, Policy, SecurityPolicy};
172 use xrust::transform::callable::FormalParameters;
173 use xrust::transform::context::StaticContextBuilder;
174 use xrust::trees::smite::RNode;
175 use xrust::xslt::from_document;
176
177 fn make_from_str(s: &str) -> Result<RNode, Error> {
178 let d = RNode::new_document();
179 let _ = xmlparse(d.clone(), s, Some(|_: &_| Err(ParseError::Notimplemented)))?;
180 Ok(d)
181 }
182
183 #[test]
184 fn include() {
185 let mut policy: Policy<RNode> = Policy::new(QName::from_local_name(
187 NcName::try_from("test_policy").unwrap(),
188 ));
189 policy.add(
190 QName::new_from_parts(
191 NcName::try_from("access").unwrap(),
192 Some(
193 NamespaceUri::try_from(
194 "http://gitlab.gnome.org/World/Rust/markup-rs/xrust-net/security",
195 )
196 .unwrap(),
197 ),
198 ),
199 Feature::new(
200 Transform::LiteralElement(
201 QName::new_from_parts(
202 NcName::try_from("permitted").unwrap(),
203 Some(
204 NamespaceUri::try_from(
205 "http://gitlab.gnome.org/World/Rust/markup-rs/Security",
206 )
207 .unwrap(),
208 ),
209 ),
210 Box::new(Transform::<RNode>::Empty),
211 ),
212 FormalParameters::Named(vec![(PARAM_URL.clone(), None)]),
213 ),
214 );
215 policy.add(
216 QName::new_from_parts(
217 NcName::try_from("maximum-size").unwrap(),
218 Some(
219 NamespaceUri::try_from(
220 "http://gitlab.gnome.org/World/Rust/markup-rs/xrust-net/security",
221 )
222 .unwrap(),
223 ),
224 ),
225 Feature::new(
226 Transform::LiteralElement(
227 QName::new_from_parts(
228 NcName::try_from("permitted").unwrap(),
229 Some(
230 NamespaceUri::try_from(
231 "http://gitlab.gnome.org/World/Rust/markup-rs/Security",
232 )
233 .unwrap(),
234 ),
235 ),
236 Box::new(Transform::<RNode>::Empty),
237 ),
238 FormalParameters::Named(vec![(PARAM_URL.clone(), None)]),
239 ),
240 );
241 let rpolicy = Rc::new(policy);
242 let mut sc = StaticContextBuilder::new()
243 .message(|_| Ok(()))
244 .fetcher(|u| resolve(u, &rpolicy))
245 .parser(|s| make_from_str(s))
246 .build();
247
248 let src = Item::Node(
249 make_from_str("<Test>one<Level1/>two<Level2/>three<Level3/>four<Level4/></Test>")
250 .expect("unable to parse source document"),
251 );
252
253 let style = make_from_str(
254 "<xsl:stylesheet xmlns:xsl='http://www.w3.org/1999/XSL/Transform'>
255 <xsl:include href='included.xsl'/>
256 <xsl:template match='child::Test'><xsl:apply-templates/></xsl:template>
257 <xsl:template match='child::Level1'>found Level1 element</xsl:template>
258 <xsl:template match='child::text()'><xsl:sequence select='.'/></xsl:template>
259</xsl:stylesheet>",
260 )
261 .expect("unable to parse stylesheet");
262
263 let pwd = std::env::current_dir().expect("unable to get current directory");
265 let pwds = pwd
266 .into_os_string()
267 .into_string()
268 .expect("unable to convert pwd");
269 let mut ctxt = from_document(
270 style,
271 Some(
272 Url::parse(format!("file://{}/tests/xsl/including.xsl", pwds.as_str()).as_str())
273 .expect("unable to parse URL"),
274 ),
275 |s| make_from_str(s),
276 |url| resolve(url, &rpolicy),
277 )
278 .expect("failed to compile stylesheet");
279
280 let rd = RNode::new_document();
281
282 ctxt.context(vec![src], 0);
283 ctxt.result_document(rd.clone());
284
285 let seq = ctxt.evaluate(&mut sc).expect("evaluation failed");
286
287 assert_eq!(
288 seq.to_xml(),
289 "onefound Level1 elementtwofound Level2 elementthreefound Level3 elementfour"
290 )
291 }
292
293 #[test]
295 fn local_only() {
296 let poldoc = make_from_str(r##"<sec:policy name='localonly' xmlns:sec='http://gitlab.gnome.org/World/Rust/markup-rs/Security'
297 xmlns:net='http://gitlab.gnome.org/World/Rust/markup-rs/xrust-net/security'
298 xmlns:xsl='http://www.w3.org/1999/XSL/Transform'>
299 <sec:feature name='net:access'>
300 <xsl:param name='url'/>
301 <xsl:choose>
302 <xsl:when test='starts-with($url, "file://")'>
303 <sec:permitted/>
304 </xsl:when>
305 <xsl:otherwise>
306 <sec:not-permitted/>
307 </xsl:otherwise>
308 </xsl:choose>
309 </sec:feature>
310 <sec:feature name='net:maximum-size'>
311 <sec:permitted/>
312 </sec:feature>
313 </sec:policy>"##).expect("unable to parse security policy");
314 let policy = poldoc
315 .to_policy()
316 .expect("unable to compile security policy");
317 let rpolicy = Rc::new(policy);
318
319 let mut sc = StaticContextBuilder::new()
320 .message(|_| Ok(()))
321 .fetcher(|u| resolve(u, &rpolicy))
322 .parser(|s| make_from_str(s))
323 .build();
324
325 let src = Item::Node(
326 make_from_str("<Test>one<Level1/>two<Level2/>three<Level3/>four<Level4/></Test>")
327 .expect("unable to parse source document"),
328 );
329
330 let style1 = make_from_str(
331 "<xsl:stylesheet xmlns:xsl='http://www.w3.org/1999/XSL/Transform'>
332 <xsl:include href='included.xsl'/>
333 <xsl:template match='child::Test'><xsl:apply-templates/></xsl:template>
334 <xsl:template match='child::Level1'>found Level1 element</xsl:template>
335 <xsl:template match='child::text()'><xsl:sequence select='.'/></xsl:template>
336</xsl:stylesheet>",
337 )
338 .expect("unable to parse stylesheet");
339
340 let pwd = std::env::current_dir().expect("unable to get current directory");
342 let pwds = pwd
343 .into_os_string()
344 .into_string()
345 .expect("unable to convert pwd");
346 let mut ctxt1 = from_document(
347 style1,
348 Some(
349 Url::parse(format!("file://{}/tests/xsl/including.xsl", pwds.as_str()).as_str())
350 .expect("unable to parse URL"),
351 ),
352 |s| make_from_str(s),
353 |url| resolve(url, &rpolicy),
354 )
355 .expect("failed to compile stylesheet");
356
357 let rd1 = RNode::new_document();
358
359 ctxt1.context(vec![src.clone()], 0);
360 ctxt1.result_document(rd1.clone());
361
362 let seq1 = ctxt1.evaluate(&mut sc).expect("evaluation failed");
363
364 assert_eq!(
365 seq1.to_xml(),
366 "onefound Level1 elementtwofound Level2 elementthreefound Level3 elementfour"
367 );
368
369 let style2 = make_from_str(
370 "<xsl:stylesheet xmlns:xsl='http://www.w3.org/1999/XSL/Transform'>
371 <xsl:include href='http://example.org/xslt/included.xsl'/>
372 <xsl:template match='child::Test'><xsl:apply-templates/></xsl:template>
373 <xsl:template match='child::Level1'>found Level1 element</xsl:template>
374 <xsl:template match='child::text()'><xsl:sequence select='.'/></xsl:template>
375</xsl:stylesheet>",
376 )
377 .expect("unable to parse stylesheet");
378
379 let result = from_document(
381 style2,
382 Some(
383 Url::parse("http://example.org/tests/xsl/including.xsl")
384 .expect("unable to parse URL"),
385 ),
386 |s| make_from_str(s),
387 |url| resolve(url, &rpolicy),
388 );
389 assert!(result.is_err())
390 }
391}