Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,9 @@ features = ["full","extra-traits"]
version = "2.5.4"
features = ["serde"]

[workspace.dependencies.urlencoding]
version = "2.1"

[workspace.dependencies.uuid]
version = "1.11.0"
features = ["v4","serde"]
Expand Down
1 change: 1 addition & 0 deletions engine/packages/guard/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ tracing.workspace = true
universaldb.workspace = true
universalpubsub.workspace = true
url.workspace = true
urlencoding.workspace = true
uuid.workspace = true

[dev-dependencies]
Expand Down
10 changes: 8 additions & 2 deletions engine/packages/guard/src/routing/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,9 +208,15 @@ pub fn parse_actor_path(path: &str) -> Option<ActorPathInfo> {
return None;
}

(aid.to_string(), Some(tok.to_string()))
// URL-decode both actor_id and token
let decoded_aid = urlencoding::decode(aid).ok()?.to_string();
let decoded_tok = urlencoding::decode(tok).ok()?.to_string();

(decoded_aid, Some(decoded_tok))
} else {
(actor_id_segment.to_string(), None)
// URL-decode actor_id
let decoded_aid = urlencoding::decode(actor_id_segment).ok()?.to_string();
(decoded_aid, None)
};

// Calculate the position in the original path where remaining path starts
Expand Down
42 changes: 41 additions & 1 deletion engine/packages/guard/tests/parse_actor_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,54 @@ fn test_parse_actor_path_special_characters() {

#[test]
fn test_parse_actor_path_encoded_characters() {
// URL encoded characters in path
// URL encoded characters in remaining path
let path = "/gateway/actor-123/api%20endpoint/test%2Fpath";
let result = parse_actor_path(path).unwrap();
assert_eq!(result.actor_id, "actor-123");
assert_eq!(result.token, None);
assert_eq!(result.remaining_path, "/api%20endpoint/test%2Fpath");
}

#[test]
fn test_parse_actor_path_encoded_actor_id() {
// URL encoded characters in actor_id (e.g., actor-123 with hyphen encoded)
let path = "/gateway/actor%2D123/endpoint";
let result = parse_actor_path(path).unwrap();
assert_eq!(result.actor_id, "actor-123");
assert_eq!(result.token, None);
assert_eq!(result.remaining_path, "/endpoint");
}

#[test]
fn test_parse_actor_path_encoded_token() {
// URL encoded characters in token (e.g., @ symbol encoded in token)
let path = "/gateway/actor-123@tok%40en/endpoint";
let result = parse_actor_path(path).unwrap();
assert_eq!(result.actor_id, "actor-123");
assert_eq!(result.token, Some("tok@en".to_string()));
assert_eq!(result.remaining_path, "/endpoint");
}

#[test]
fn test_parse_actor_path_encoded_actor_id_and_token() {
// URL encoded characters in both actor_id and token
let path = "/gateway/actor%2D123@token%2Dwith%2Dencoded/endpoint";
let result = parse_actor_path(path).unwrap();
assert_eq!(result.actor_id, "actor-123");
assert_eq!(result.token, Some("token-with-encoded".to_string()));
assert_eq!(result.remaining_path, "/endpoint");
}

#[test]
fn test_parse_actor_path_encoded_spaces() {
// URL encoded spaces in actor_id
let path = "/gateway/actor%20with%20spaces/endpoint";
let result = parse_actor_path(path).unwrap();
assert_eq!(result.actor_id, "actor with spaces");
assert_eq!(result.token, None);
assert_eq!(result.remaining_path, "/endpoint");
}

// Invalid path tests

#[test]
Expand Down
23 changes: 19 additions & 4 deletions rivetkit-typescript/packages/rivetkit/src/manager/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,16 +399,31 @@ export function parseActorPath(path: string): ActorPathInfo | null {
const atPos = actorSegment.indexOf("@");
if (atPos !== -1) {
// Pattern: /gateway/{actor_id}@{token}/{...path}
actorId = actorSegment.slice(0, atPos);
token = actorSegment.slice(atPos + 1);
const rawActorId = actorSegment.slice(0, atPos);
const rawToken = actorSegment.slice(atPos + 1);

// Check for empty actor_id or token
if (actorId.length === 0 || token.length === 0) {
if (rawActorId.length === 0 || rawToken.length === 0) {
return null;
}

// URL-decode both actor_id and token
try {
actorId = decodeURIComponent(rawActorId);
token = decodeURIComponent(rawToken);
} catch (e) {
// Invalid URL encoding
return null;
}
} else {
// Pattern: /gateway/{actor_id}/{...path}
actorId = actorSegment;
// URL-decode actor_id
try {
actorId = decodeURIComponent(actorSegment);
} catch (e) {
// Invalid URL encoding
return null;
}
token = undefined;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,64 @@ describe("parseActorPath", () => {
});
});

describe("URL-encoded actor_id and token", () => {
test("should decode URL-encoded characters in actor_id", () => {
const path = "/gateway/actor%2D123/endpoint";
const result = parseActorPath(path);

expect(result).not.toBeNull();
expect(result?.actorId).toBe("actor-123");
expect(result?.token).toBeUndefined();
expect(result?.remainingPath).toBe("/endpoint");
});

test("should decode URL-encoded characters in token", () => {
const path = "/gateway/actor-123@tok%40en/endpoint";
const result = parseActorPath(path);

expect(result).not.toBeNull();
expect(result?.actorId).toBe("actor-123");
expect(result?.token).toBe("tok@en");
expect(result?.remainingPath).toBe("/endpoint");
});

test("should decode URL-encoded characters in both actor_id and token", () => {
const path = "/gateway/actor%2D123@token%2Dwith%2Dencoded/endpoint";
const result = parseActorPath(path);

expect(result).not.toBeNull();
expect(result?.actorId).toBe("actor-123");
expect(result?.token).toBe("token-with-encoded");
expect(result?.remainingPath).toBe("/endpoint");
});

test("should decode URL-encoded spaces in actor_id", () => {
const path = "/gateway/actor%20with%20spaces/endpoint";
const result = parseActorPath(path);

expect(result).not.toBeNull();
expect(result?.actorId).toBe("actor with spaces");
expect(result?.token).toBeUndefined();
expect(result?.remainingPath).toBe("/endpoint");
});

test("should reject invalid URL encoding in actor_id", () => {
// %ZZ is invalid hex
const path = "/gateway/actor%ZZ123/endpoint";
const result = parseActorPath(path);

expect(result).toBeNull();
});

test("should reject invalid URL encoding in token", () => {
// %GG is invalid hex
const path = "/gateway/actor-123@token%GG/endpoint";
const result = parseActorPath(path);

expect(result).toBeNull();
});
});

describe("Invalid paths - wrong prefix", () => {
test("should reject path with wrong prefix", () => {
expect(parseActorPath("/api/123/endpoint")).toBeNull();
Expand Down
Loading