Skip to content
Merged
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
1 change: 1 addition & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ bytes = "1"
rand = "0.8"
regex = "1"
base64 = "0.22"
url = "2"

# Crypto (M6 v3 CP register flow + ApiKey hash auth)
sha2 = "0.10"
Expand Down
3 changes: 3 additions & 0 deletions crates/aisix-proxy/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ async-trait.workspace = true
async-stream = "0.3"
dashmap.workspace = true
base64.workspace = true
# Scheme validation of provider-supplied redirect targets on
# `GET /v1/videos/:id/content` (already in the tree via reqwest).
url.workspace = true
# Per-request weighted-random target selection in `routing::weighted_pick`.
# `thread_rng()` gives proper per-request entropy that converges to the
# configured weights over a finite sample (fix for #197 — the prior
Expand Down
9 changes: 9 additions & 0 deletions crates/aisix-proxy/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,13 @@ pub enum ProxyError {
ApiKeyDisabled,
#[error("model {0:?} not found")]
ModelNotFound(String),
/// A `/v1/videos/{video_id}` id that this gateway could not have
/// minted — undecodable, or referencing a Model entry that no longer
/// exists in the snapshot. 404, mirroring how the upstream videos
/// API treats unknown job ids. The id echoes back verbatim: the
/// caller supplied it, so it leaks nothing.
#[error("video {0:?} not found")]
VideoNotFound(String),
#[error("API key is not allowed to use model {0:?}")]
ModelForbidden(String),
/// The resolved client IP is outside the model's `allowed_cidrs`
Expand Down Expand Up @@ -240,6 +247,7 @@ impl ProxyError {
ProxyError::ModelForbidden(_) => StatusCode::FORBIDDEN,
ProxyError::ModelIpRestricted(_) => StatusCode::FORBIDDEN,
ProxyError::ModelNotFound(_) => StatusCode::NOT_FOUND,
ProxyError::VideoNotFound(_) => StatusCode::NOT_FOUND,
ProxyError::InvalidRequest(_) => StatusCode::BAD_REQUEST,
ProxyError::ProviderUnavailable => StatusCode::SERVICE_UNAVAILABLE,
ProxyError::AllCandidatesUnavailable { .. } => StatusCode::SERVICE_UNAVAILABLE,
Expand All @@ -262,6 +270,7 @@ impl ProxyError {
ProxyError::ModelForbidden(_) => "permission_denied",
ProxyError::ModelIpRestricted(_) => "permission_denied",
ProxyError::ModelNotFound(_) => "model_not_found",
ProxyError::VideoNotFound(_) => "video_not_found",
ProxyError::InvalidRequest(_) => "invalid_request_error",
ProxyError::RequestTooLarge { .. } => "invalid_request_error",
ProxyError::ProviderUnavailable => "provider_unavailable",
Expand Down
13 changes: 11 additions & 2 deletions crates/aisix-proxy/src/jobs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,15 @@ pub(crate) fn decode_routed_id(id: &str) -> Option<(String, String)> {

/// Charset guard for ids interpolated into upstream URL paths. A decoded
/// (attacker-suppliable) id must never smuggle path separators or query
/// metacharacters into the upstream URL.
fn require_safe_upstream_id(raw: &str) -> Result<(), ProxyError> {
/// metacharacters into the upstream URL. `pub(crate)`: the videos
/// surface applies the same guard to decoded upstream task ids.
pub(crate) fn require_safe_upstream_id(raw: &str) -> Result<(), ProxyError> {
let ok = !raw.is_empty()
&& raw.len() <= 256
// `.` / `..` are valid under the charset but are path-segment
// aliases some upstream routers normalise — never forward them.
&& raw != "."
&& raw != ".."
&& raw
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | ':'));
Expand Down Expand Up @@ -1795,6 +1800,10 @@ mod tests {
"file#frag",
"file abc",
"file&x=1",
// Bare path-segment aliases: valid charset, but some
// upstream routers normalise them into the parent path.
".",
"..",
] {
assert!(
require_safe_upstream_id(bad).is_err(),
Expand Down
8 changes: 8 additions & 0 deletions crates/aisix-proxy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ mod stream_timeout;
mod token_estimate;
mod usage_attr;
mod util;
mod videos;

pub use auth::AuthenticatedKey;
pub use error::{ErrorEnvelope, ProxyError};
Expand Down Expand Up @@ -112,6 +113,13 @@ pub fn build_router(state: ProxyState) -> Router {
.route("/v1/audio/transcriptions", post(audio::transcriptions))
.route("/v1/audio/translations", post(audio::translations))
.route("/v1/audio/speech", post(audio::speech))
// Unified video-generation surface (AISIX-Cloud#1118 Phase 1):
// submit → poll → fetch. Auth/ACL/quota enforced inside the
// handlers; the GET routes are exempt from model-level rate
// limits by design (see videos.rs).
.route("/v1/videos", post(videos::create_video))
.route("/v1/videos/:id", get(videos::get_video))
.route("/v1/videos/:id/content", get(videos::video_content))
// OpenAI Realtime WebSocket relay (#721). Auth/ACL/quota are
// enforced pre-upgrade inside the handler.
.route("/v1/realtime", get(realtime::realtime))
Expand Down
Loading
Loading