Skip to content

Commit 1a140ab

Browse files
committed
Refactor to make it possible to expose into JS properly.
1 parent a186b4f commit 1a140ab

File tree

24 files changed

+976
-427
lines changed

24 files changed

+976
-427
lines changed

__test__/handler.spec.mjs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import test from 'ava'
33
import { Php, Request } from '../index.js'
44

55
import { MockRoot } from './util.mjs'
6+
import { Rewriter } from '../index.js'
67

78
test('Support input/output streams', async (t) => {
89
const mockroot = await MockRoot.from({
@@ -167,3 +168,42 @@ test('Allow receiving true errors', async (t) => {
167168
message: /^Script not found: .*\/index\.php$/
168169
}, 'should throw error')
169170
})
171+
172+
test('Accept rewriter', async (t) => {
173+
const mockroot = await MockRoot.from({
174+
'index.php': '<?php echo "Hello, World!"; ?>'
175+
})
176+
t.teardown(() => mockroot.clean())
177+
178+
const rewrite = new Rewriter([
179+
{
180+
conditions: [
181+
{
182+
type: 'path',
183+
args: ['^/rewrite_me$']
184+
}
185+
],
186+
rewriters: [
187+
{
188+
type: 'path',
189+
args: ['^/rewrite_me$', '/index.php']
190+
}
191+
]
192+
}
193+
])
194+
195+
const php = new Php({
196+
argv: process.argv,
197+
docroot: mockroot.path,
198+
throwRequestErrors: true,
199+
rewrite
200+
})
201+
202+
const req = new Request({
203+
url: 'http://example.com/rewrite_me'
204+
})
205+
206+
const res = await php.handleRequest(req)
207+
t.is(res.status, 200)
208+
t.is(res.body.toString('utf8'), 'Hello, World!')
209+
})

__test__/rewriter.spec.mjs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,25 @@ test('rewrites URLs', (t) => {
1111
}
1212
})
1313

14+
const config = {
15+
condition: {
16+
$and: [
17+
{
18+
type: 'path',
19+
args: ['^/index.php$']
20+
},
21+
{
22+
type: 'header',
23+
args: ['TEST', '^foo$']
24+
}
25+
]
26+
},
27+
rewrite: {
28+
type: 'path',
29+
args: ['^(/index.php)$', '/foo$1']
30+
}
31+
}
32+
1433
const rewriter = new Rewriter([
1534
{
1635
operation: 'and',
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
use super::{Condition, Request};
2+
3+
impl<F> Condition for F
4+
where
5+
F: Fn(&Request) -> bool + Sync + Send,
6+
{
7+
/// Matches if calling the Fn(&Request) with the given request returns true
8+
///
9+
/// # Examples
10+
///
11+
/// ```
12+
/// # use lang_handler::{Request, rewrite::Condition};
13+
/// let condition = |request: &Request| {
14+
/// request.url().path().contains("/foo")
15+
/// };
16+
///
17+
/// let request = Request::builder()
18+
/// .url("http://example.com/index.php")
19+
/// .build()
20+
/// .expect("request should build");
21+
///
22+
/// assert_eq!(condition.matches(&request), false);
23+
/// ```
24+
fn matches(&self, request: &Request) -> bool {
25+
self(request)
26+
}
27+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
use std::path::PathBuf;
2+
3+
use super::Condition;
4+
use super::Request;
5+
6+
/// Match if request path exists
7+
#[derive(Clone, Debug)]
8+
pub struct ExistenceCondition(PathBuf);
9+
10+
impl ExistenceCondition {
11+
/// Construct an ExistenceCondition to check within a given base directory.
12+
///
13+
/// # Examples
14+
///
15+
/// ```
16+
/// # use lang_handler::rewrite::{Condition, ExistenceCondition};
17+
/// # use lang_handler::Request;
18+
/// let condition = ExistenceCondition::new("/foo/bar");
19+
/// ```
20+
pub fn new<P>(base: P) -> Self
21+
where
22+
P: Into<PathBuf>,
23+
{
24+
Self(base.into())
25+
}
26+
}
27+
28+
impl Condition for ExistenceCondition {
29+
/// A NonExistenceCondition matches a request if the path segment of the
30+
/// request url does not exist in the provided base directory.
31+
///
32+
/// # Examples
33+
///
34+
/// ```
35+
/// # use lang_handler::rewrite::{Condition, ExistenceCondition};
36+
/// # use lang_handler::Request;
37+
/// let condition = ExistenceCondition::new("/foo/bar");
38+
///
39+
/// let request = Request::builder()
40+
/// .url("http://example.com/index.php")
41+
/// .build()
42+
/// .expect("should build request");
43+
///
44+
/// assert_eq!(condition.matches(&request), false);
45+
/// ```
46+
fn matches(&self, request: &Request) -> bool {
47+
self
48+
.0
49+
.join(request.url().path().strip_prefix("/").unwrap())
50+
.canonicalize()
51+
.is_ok()
52+
}
53+
}
54+
55+
/// Match if request path does not exist
56+
#[derive(Clone, Debug, Default)]
57+
pub struct NonExistenceCondition(PathBuf);
58+
59+
impl NonExistenceCondition {
60+
/// Construct a NonExistenceCondition to check within a given base directory.
61+
///
62+
/// # Examples
63+
///
64+
/// ```
65+
/// # use lang_handler::rewrite::{Condition, NonExistenceCondition};
66+
/// # use lang_handler::Request;
67+
/// let condition = NonExistenceCondition::new("/foo/bar");
68+
/// ```
69+
pub fn new<P>(base: P) -> Self
70+
where
71+
P: Into<PathBuf>,
72+
{
73+
Self(base.into())
74+
}
75+
}
76+
77+
impl Condition for NonExistenceCondition {
78+
/// A NonExistenceCondition matches a request if the path segment of the
79+
/// request url does not exist in the provided base directory.
80+
///
81+
/// # Examples
82+
///
83+
/// ```
84+
/// # use lang_handler::rewrite::{Condition, NonExistenceCondition};
85+
/// # use lang_handler::Request;
86+
/// let condition = NonExistenceCondition::new("/foo/bar");
87+
///
88+
/// let request = Request::builder()
89+
/// .url("http://example.com/index.php")
90+
/// .build()
91+
/// .expect("should build request");
92+
///
93+
/// assert!(condition.matches(&request));
94+
/// ```
95+
fn matches(&self, request: &Request) -> bool {
96+
self
97+
.0
98+
.join(request.url().path().strip_prefix("/").unwrap())
99+
.canonicalize()
100+
.is_err()
101+
}
102+
}
103+
104+
#[cfg(test)]
105+
mod test {
106+
use super::*;
107+
108+
use std::{
109+
env::current_dir,
110+
fs::File,
111+
io::Write,
112+
path::{Path, PathBuf},
113+
};
114+
115+
struct TempFile(PathBuf);
116+
117+
impl TempFile {
118+
fn new<P: AsRef<Path>, S: Into<String>>(path: P, contents: S) -> Self {
119+
let mut file = File::create(path.as_ref()).unwrap();
120+
file.write_all(contents.into().as_bytes()).unwrap();
121+
Self(path.as_ref().to_owned())
122+
}
123+
}
124+
125+
impl Drop for TempFile {
126+
fn drop(&mut self) {
127+
std::fs::remove_file(&self.0).unwrap();
128+
}
129+
}
130+
131+
#[test]
132+
fn test_existence_condition() {
133+
let _temp = TempFile::new("exists.php", "<?php echo \"Hello, world!\"; ?>");
134+
135+
let cwd = current_dir().unwrap();
136+
let condition = ExistenceCondition::new(cwd);
137+
138+
let request = Request::builder()
139+
.url("http://example.com/exists.php")
140+
.build()
141+
.expect("request should build");
142+
143+
assert!(condition.matches(&request));
144+
}
145+
146+
#[test]
147+
fn test_non_existence_condition() {
148+
let cwd = current_dir().unwrap();
149+
let condition = NonExistenceCondition::new(cwd);
150+
151+
let request = Request::builder()
152+
.url("http://example.com/does_not_exist.php")
153+
.build()
154+
.expect("request should build");
155+
156+
assert!(condition.matches(&request));
157+
}
158+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
use super::{Condition, Request};
2+
3+
/// This provides logical grouping of conditions using either AND or OR
4+
/// combination behaviours.
5+
pub enum ConditionGroup<A, B>
6+
where
7+
A: Condition + ?Sized,
8+
B: Condition + ?Sized,
9+
{
10+
Or(Box<A>, Box<B>),
11+
And(Box<A>, Box<B>),
12+
}
13+
14+
impl<A, B> ConditionGroup<A, B>
15+
where
16+
A: Condition + ?Sized,
17+
B: Condition + ?Sized,
18+
{
19+
pub fn and(a: Box<A>, b: Box<B>) -> Box<Self> {
20+
Box::new(ConditionGroup::And(a, b))
21+
}
22+
23+
pub fn or(a: Box<A>, b: Box<B>) -> Box<Self> {
24+
Box::new(ConditionGroup::Or(a, b))
25+
}
26+
}
27+
28+
impl<A, B> Condition for ConditionGroup<A, B>
29+
where
30+
A: Condition + ?Sized,
31+
B: Condition + ?Sized,
32+
{
33+
fn matches(&self, request: &Request) -> bool {
34+
match self {
35+
ConditionGroup::Or(a, b) => a.matches(request) || b.matches(request),
36+
ConditionGroup::And(a, b) => a.matches(request) && b.matches(request),
37+
}
38+
}
39+
}
40+
41+
#[cfg(test)]
42+
mod test {
43+
use super::*;
44+
use crate::rewrite::{ConditionExt, HeaderCondition, PathCondition};
45+
46+
#[test]
47+
fn test_condition_group_and() {
48+
let header = HeaderCondition::new("TEST", "^foo$").expect("should be valid regex");
49+
50+
let path = PathCondition::new("^/index\\.php$").expect("should be valid regex");
51+
52+
let header_and_path = header.and(path);
53+
54+
// Check it matches when all conditions match
55+
let request = Request::builder()
56+
.url("http://example.com/index.php")
57+
.header("TEST", "foo")
58+
.build()
59+
.expect("request should build");
60+
61+
assert!(header_and_path.matches(&request));
62+
63+
// Check it _does not_ match if either condition does not match
64+
let only_header = Request::builder()
65+
.url("http://example.com/nope.php")
66+
.header("TEST", "foo")
67+
.build()
68+
.expect("request should build");
69+
70+
assert!(!header_and_path.matches(&only_header));
71+
72+
let only_url = Request::builder()
73+
.url("http://example.com/index.php")
74+
.build()
75+
.expect("request should build");
76+
77+
assert!(!header_and_path.matches(&only_url));
78+
}
79+
80+
#[test]
81+
fn test_condition_group_or() {
82+
let header = HeaderCondition::new("TEST", "^foo$").expect("should be valid regex");
83+
84+
let path = PathCondition::new("^/index\\.php$").expect("should be valid regex");
85+
86+
let header_or_path = header.or(path);
87+
88+
// Check it matches when either condition matches
89+
let request = Request::builder()
90+
.url("http://example.com/index.php")
91+
.header("TEST", "foo")
92+
.build()
93+
.expect("request should build");
94+
95+
assert!(header_or_path.matches(&request));
96+
97+
// Check it matches if either condition does not match
98+
let only_header = Request::builder()
99+
.url("http://example.com/nope.php")
100+
.header("TEST", "foo")
101+
.build()
102+
.expect("request should build");
103+
104+
assert!(header_or_path.matches(&only_header));
105+
106+
let only_url = Request::builder()
107+
.url("http://example.com/index.php")
108+
.build()
109+
.expect("request should build");
110+
111+
assert!(header_or_path.matches(&only_url));
112+
}
113+
}

0 commit comments

Comments
 (0)