rtsp server: allow custom authentication

Enables subclassing gst_rtsp_server::RTSPAuth and overriding its
authenticate/check/generate_authenticate_header methods

Also add new methods in RTSPContext to retrieve RTSP request/response, and to
get/replace tokens.

Additionally, added RTSPMessage with methods to add an authentication
header to a request / retrieve authentication parameters from a
response.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer-rs/-/merge_requests/1359>
This commit is contained in:
olivierbabasse 2023-12-08 16:48:03 +01:00 committed by Sebastian Dröge
parent 08fa853c7e
commit 60e8c44abb
14 changed files with 588 additions and 4 deletions

7
Cargo.lock generated
View file

@ -388,6 +388,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991"
[[package]]
name = "data-encoding"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
[[package]]
name = "derive_more"
version = "0.99.17"
@ -474,6 +480,7 @@ dependencies = [
"byte-slice-cast",
"cairo-rs",
"cocoa",
"data-encoding",
"derive_more",
"futures",
"gio",

View file

@ -41,6 +41,7 @@ raw-window-handle = { version = "0.5", optional = true }
uds = { version = "0.4", optional = true }
winit = { version = "0.29", optional = true, default-features = false, features = ["rwh_05"] }
atomic_refcell = "0.1"
data-encoding = "2.0"
[target.'cfg(windows)'.dependencies]
windows = { version = "0.52", features=["Win32_Graphics_Direct3D11",
@ -133,6 +134,10 @@ required-features = ["rtsp-server"]
name = "rtsp-server-subclass"
required-features = ["rtsp-server"]
[[bin]]
name = "rtsp-server-custom-auth"
required-features = ["rtsp-server", "gst-rtsp-server/v1_22"]
[[bin]]
name = "tagsetter"

View file

@ -0,0 +1,219 @@
// This example demonstrates how to set up a rtsp server using GStreamer
// and extending the default auth module behaviour by subclassing RTSPAuth
// For this, the example creates a videotestsrc pipeline manually to be used
// by the RTSP server for providing data
#![allow(clippy::non_send_fields_in_send_ty)]
use anyhow::Error;
use derive_more::{Display, Error};
use gst_rtsp_server::prelude::*;
#[path = "../examples-common.rs"]
mod examples_common;
#[derive(Debug, Display, Error)]
#[display(fmt = "Could not get mount points")]
struct NoMountPoints;
fn main_loop() -> Result<(), Error> {
let main_loop = glib::MainLoop::new(None, false);
let server = gst_rtsp_server::RTSPServer::new();
// We create our custom auth module.
// The job of the auth module is to authenticate users and authorize
// factories access/construction.
let auth = auth::Auth::default();
server.set_auth(Some(&auth));
// Much like HTTP servers, RTSP servers have multiple endpoints that
// provide different streams. Here, we ask our server to give
// us a reference to his list of endpoints, so we can add our
// test endpoint, providing the pipeline from the cli.
let mounts = server.mount_points().ok_or(NoMountPoints)?;
// Next, we create a factory for the endpoint we want to create.
// The job of the factory is to create a new pipeline for each client that
// connects, or (if configured to do so) to reuse an existing pipeline.
let factory = gst_rtsp_server::RTSPMediaFactory::new();
// Here we tell the media factory the media we want to serve.
// This is done in the launch syntax. When the first client connects,
// the factory will use this syntax to create a new pipeline instance.
factory.set_launch("( videotestsrc ! vp8enc ! rtpvp8pay name=pay0 )");
// This setting specifies whether each connecting client gets the output
// of a new instance of the pipeline, or whether all connected clients share
// the output of the same pipeline.
// If you want to stream a fixed video you have stored on the server to any
// client, you would not set this to shared here (since every client wants
// to start at the beginning of the video). But if you want to distribute
// a live source, you will probably want to set this to shared, to save
// computing and memory capacity on the server.
factory.set_shared(true);
// Now we add a new mount-point and tell the RTSP server to serve the content
// provided by the factory we configured above, when a client connects to
// this specific path.
mounts.add_factory("/test", factory);
// Attach the server to our main context.
// A main context is the thing where other stuff is registering itself for its
// events (e.g. sockets, GStreamer bus, ...) and the main loop is something that
// polls the main context for its events and dispatches them to whoever is
// interested in them. In this example, we only do have one, so we can
// leave the context parameter empty, it will automatically select
// the default one.
let id = server.attach(None)?;
println!(
"Stream ready at rtsp://127.0.0.1:{}/test",
server.bound_port()
);
println!("user admin/password can access stream");
println!("user demo/demo passes authentication but receives 404");
println!("other users do not pass pass authentication and receive 401");
// Start the mainloop. From this point on, the server will start to serve
// our quality content to connecting clients.
main_loop.run();
id.remove();
Ok(())
}
// Our custom auth module
mod auth {
// In the imp submodule we include the actual implementation
mod imp {
use gst_rtsp::{RTSPHeaderField, RTSPStatusCode};
use gst_rtsp_server::{prelude::*, subclass::prelude::*, RTSPContext};
// This is the private data of our auth
#[derive(Default)]
pub struct Auth;
impl Auth {
// Simulate external auth validation and user extraction
// authorized users are admin/password and demo/demo
fn external_auth(&self, auth: &str) -> Option<String> {
if let Ok(decoded) = data_encoding::BASE64.decode(auth.as_bytes()) {
if let Ok(decoded) = std::str::from_utf8(&decoded) {
let tokens = decoded.split(':').collect::<Vec<_>>();
if tokens == vec!["admin", "password"] || tokens == vec!["demo", "demo"] {
return Some(tokens[0].into());
}
}
}
None
}
// Simulate external role check
// admin user can construct and access media factory
fn external_access_check(&self, user: &str) -> bool {
user == "admin"
}
}
// This trait registers our type with the GObject object system and
// provides the entry points for creating a new instance and setting
// up the class data
#[glib::object_subclass]
impl ObjectSubclass for Auth {
const NAME: &'static str = "RsRTSPAuth";
type Type = super::Auth;
type ParentType = gst_rtsp_server::RTSPAuth;
}
// Implementation of glib::Object virtual methods
impl ObjectImpl for Auth {}
// Implementation of gst_rtsp_server::RTSPAuth virtual methods
impl RTSPAuthImpl for Auth {
fn authenticate(&self, ctx: &RTSPContext) -> bool {
// authenticate should always be called with a valid context request
let req = ctx
.request()
.expect("Context without request. Should not happen !");
if let Some(auth_credentials) = req.parse_auth_credentials().get(0) {
if let Some(authorization) = auth_credentials.authorization() {
if let Some(user) = self.external_auth(authorization) {
// Update context token with authenticated username
ctx.set_token(gst_rtsp_server::RTSPToken::new(&[("user", &user)]));
return true;
}
}
}
false
}
fn check(&self, ctx: &RTSPContext, role: &glib::GString) -> bool {
// We only check media factory access
if !role.starts_with("auth.check.media.factory") {
return true;
}
if ctx.token().is_none() {
// If we do not have a context token yet, check if there are any auth credentials in request
if !self.authenticate(ctx) {
// If there were no credentials, send a "401 Unauthorized" response
if let Some(resp) = ctx.response() {
resp.init_response(RTSPStatusCode::Unauthorized, ctx.request());
resp.add_header(
RTSPHeaderField::WwwAuthenticate,
"Basic realm=\"CustomRealm\"",
);
if let Some(client) = ctx.client() {
client.send_message(resp, ctx.session());
}
}
return false;
}
}
if let Some(token) = ctx.token() {
// If we already have a user token...
if self.external_access_check(&token.string("user").unwrap_or_default()) {
// grant access if user may access factory
return true;
} else {
// send a "404 Not Found" response if user may not access factory
if let Some(resp) = ctx.response() {
resp.init_response(RTSPStatusCode::NotFound, ctx.request());
if let Some(client) = ctx.client() {
client.send_message(resp, ctx.session());
}
}
}
}
false
}
}
}
// This here defines the public interface of our auth and implements
// the corresponding traits so that it behaves like any other RTSPAuth
glib::wrapper! {
pub struct Auth(ObjectSubclass<imp::Auth>) @extends gst_rtsp_server::RTSPAuth;
}
impl Default for Auth {
// Creates a new instance of our auth
fn default() -> Self {
glib::Object::new()
}
}
}
fn example_main() -> Result<(), Error> {
gst::init()?;
main_loop()
}
fn main() {
match examples_common::run(example_main) {
Ok(r) => r,
Err(e) => eprintln!("Error! {e}"),
}
}

View file

@ -1,8 +1,8 @@
// Take a look at the license at the top of the repository in the LICENSE file.
use crate::{RTSPClient, RTSPSession};
use glib::{prelude::*, source::SourceId, translate::*};
use crate::RTSPClient;
use gst_rtsp::rtsp_message::RTSPMessage;
mod sealed {
pub trait Sealed {}
@ -19,6 +19,21 @@ pub trait RTSPClientExtManual: sealed::Sealed + IsA<RTSPClient> + 'static {
))
}
}
#[doc(alias = "gst_rtsp_client_send_message")]
fn send_message(
&self,
message: &RTSPMessage,
session: Option<&RTSPSession>,
) -> gst_rtsp::RTSPResult {
unsafe {
from_glib(ffi::gst_rtsp_client_send_message(
self.as_ref().to_glib_none().0,
session.to_glib_none().0,
message.to_glib_none().0,
))
}
}
}
impl<O: IsA<RTSPClient>> RTSPClientExtManual for O {}

View file

@ -5,8 +5,10 @@ use std::{
ptr::{self, addr_of},
};
use glib::translate::*;
use gst_rtsp::RTSPUrl;
use glib::{translate::*, ObjectType};
use gst_rtsp::{rtsp_message::RTSPMessage, RTSPUrl};
use crate::{RTSPClient, RTSPSession, RTSPToken};
#[derive(Debug, PartialEq, Eq)]
#[doc(alias = "GstRTSPContext")]
@ -42,6 +44,87 @@ impl RTSPContext {
}
}
#[inline]
pub fn client(&self) -> Option<&RTSPClient> {
unsafe {
let ptr = self.0.as_ptr();
if (*ptr).client.is_null() {
None
} else {
let client = RTSPClient::from_glib_ptr_borrow(
addr_of!((*ptr).client) as *const *const ffi::GstRTSPClient
);
Some(client)
}
}
}
#[inline]
pub fn request(&self) -> Option<&RTSPMessage> {
unsafe {
let ptr = self.0.as_ptr();
if (*ptr).request.is_null() {
None
} else {
let msg = RTSPMessage::from_glib_ptr_borrow(
addr_of!((*ptr).request) as *const *const gst_rtsp::ffi::GstRTSPMessage
);
Some(msg)
}
}
}
#[inline]
pub fn response(&self) -> Option<&RTSPMessage> {
unsafe {
let ptr = self.0.as_ptr();
if (*ptr).response.is_null() {
None
} else {
let msg = RTSPMessage::from_glib_ptr_borrow(
addr_of!((*ptr).response) as *const *const gst_rtsp::ffi::GstRTSPMessage
);
Some(msg)
}
}
}
#[inline]
pub fn session(&self) -> Option<&RTSPSession> {
unsafe {
let ptr = self.0.as_ptr();
if (*ptr).session.is_null() {
None
} else {
let sess = RTSPSession::from_glib_ptr_borrow(
addr_of!((*ptr).session) as *const *const ffi::GstRTSPSession
);
Some(sess)
}
}
}
#[inline]
pub fn token(&self) -> Option<RTSPToken> {
unsafe {
let ptr = self.0.as_ptr();
if (*ptr).token.is_null() {
None
} else {
let token = RTSPToken::from_glib_none((*ptr).token as *const ffi::GstRTSPToken);
Some(token)
}
}
}
#[cfg(feature = "v1_22")]
#[doc(alias = "gst_rtsp_context_set_token")]
pub fn set_token(&self, token: RTSPToken) {
unsafe {
ffi::gst_rtsp_context_set_token(self.0.as_ptr(), token.into_glib_ptr());
}
}
// TODO: Add additional getters for all the contained fields as needed
}

View file

@ -2,6 +2,7 @@
#![allow(clippy::cast_ptr_alignment)]
mod rtsp_auth;
mod rtsp_client;
mod rtsp_media;
mod rtsp_media_factory;
@ -20,6 +21,7 @@ pub mod prelude {
pub use gst::subclass::prelude::*;
pub use super::{
rtsp_auth::{RTSPAuthImpl, RTSPAuthImplExt},
rtsp_client::{RTSPClientImpl, RTSPClientImplExt},
rtsp_media::{RTSPMediaImpl, RTSPMediaImplExt},
rtsp_media_factory::{RTSPMediaFactoryImpl, RTSPMediaFactoryImplExt},

View file

@ -0,0 +1,116 @@
// Take a look at the license at the top of the repository in the LICENSE file.
use crate::{RTSPAuth, RTSPContext};
use glib::{prelude::*, subclass::prelude::*, translate::*};
use libc::c_char;
pub trait RTSPAuthImpl: RTSPAuthImplExt + ObjectImpl + Send + Sync {
fn authenticate(&self, ctx: &RTSPContext) -> bool {
self.parent_authenticate(ctx)
}
fn check(&self, ctx: &RTSPContext, check: &glib::GString) -> bool {
self.parent_check(ctx, check)
}
fn generate_authenticate_header(&self, ctx: &RTSPContext) {
self.parent_generate_authenticate_header(ctx);
}
}
mod sealed {
pub trait Sealed {}
impl<T: super::RTSPAuthImplExt> Sealed for T {}
}
pub trait RTSPAuthImplExt: sealed::Sealed + ObjectSubclass {
fn parent_authenticate(&self, ctx: &RTSPContext) -> bool {
unsafe {
let data = Self::type_data();
let parent_class = data.as_ref().parent_class() as *mut ffi::GstRTSPAuthClass;
(*parent_class)
.authenticate
.map(|f| {
from_glib(f(
self.obj().unsafe_cast_ref::<RTSPAuth>().to_glib_none().0,
ctx.to_glib_none().0,
))
})
.unwrap_or(false)
}
}
fn parent_check(&self, ctx: &RTSPContext, check: &glib::GString) -> bool {
unsafe {
let data = Self::type_data();
let parent_class = data.as_ref().parent_class() as *mut ffi::GstRTSPAuthClass;
(*parent_class)
.check
.map(|f| {
from_glib(f(
self.obj().unsafe_cast_ref::<RTSPAuth>().to_glib_none().0,
ctx.to_glib_none().0,
check.to_glib_none().0,
))
})
.unwrap_or(false)
}
}
fn parent_generate_authenticate_header(&self, ctx: &RTSPContext) {
unsafe {
let data = Self::type_data();
let parent_class = data.as_ref().parent_class() as *mut ffi::GstRTSPAuthClass;
if let Some(f) = (*parent_class).generate_authenticate_header {
f(
self.obj().unsafe_cast_ref::<RTSPAuth>().to_glib_none().0,
ctx.to_glib_none().0,
)
}
}
}
}
impl<T: RTSPAuthImpl> RTSPAuthImplExt for T {}
unsafe impl<T: RTSPAuthImpl> IsSubclassable<T> for RTSPAuth {
fn class_init(klass: &mut glib::Class<Self>) {
Self::parent_class_init::<T>(klass);
let klass = klass.as_mut();
klass.authenticate = Some(rtsp_auth_authenticate::<T>);
klass.check = Some(rtsp_auth_check::<T>);
klass.generate_authenticate_header = Some(rtsp_auth_generate_authenticate_header::<T>);
}
}
unsafe extern "C" fn rtsp_auth_authenticate<T: RTSPAuthImpl>(
ptr: *mut ffi::GstRTSPAuth,
ctx: *mut ffi::GstRTSPContext,
) -> glib::ffi::gboolean {
let instance = &*(ptr as *mut T::Instance);
let imp = instance.imp();
imp.authenticate(&from_glib_borrow(ctx)).into_glib()
}
unsafe extern "C" fn rtsp_auth_check<T: RTSPAuthImpl>(
ptr: *mut ffi::GstRTSPAuth,
ctx: *mut ffi::GstRTSPContext,
check: *const c_char,
) -> glib::ffi::gboolean {
let instance = &*(ptr as *mut T::Instance);
let imp = instance.imp();
imp.check(&from_glib_borrow(ctx), &from_glib_borrow(check))
.into_glib()
}
unsafe extern "C" fn rtsp_auth_generate_authenticate_header<T: RTSPAuthImpl>(
ptr: *mut ffi::GstRTSPAuth,
ctx: *mut ffi::GstRTSPContext,
) {
let instance = &*(ptr as *mut T::Instance);
let imp = instance.imp();
imp.generate_authenticate_header(&from_glib_borrow(ctx));
}

View file

@ -49,6 +49,11 @@ name = "Gst.Structure"
status = "manual"
ref_mode = "ref"
[[object]]
name = "GstRtsp.RTSPAuthCredential"
status = "generate"
concurrency = "send"
[[object]]
name = "GstRtsp.RTSPAuthParam"
status = "generate"

View file

@ -3,6 +3,9 @@
// from gst-gir-files (https://gitlab.freedesktop.org/gstreamer/gir-files-rs.git)
// DO NOT EDIT
mod rtsp_auth_credential;
pub use self::rtsp_auth_credential::RTSPAuthCredential;
mod rtsp_auth_param;
pub use self::rtsp_auth_param::RTSPAuthParam;

View file

@ -0,0 +1,17 @@
// This file was generated by gir (https://github.com/gtk-rs/gir)
// from gir-files (https://github.com/gtk-rs/gir-files)
// from gst-gir-files (https://gitlab.freedesktop.org/gstreamer/gir-files-rs.git)
// DO NOT EDIT
glib::wrapper! {
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RTSPAuthCredential(Boxed<ffi::GstRTSPAuthCredential>);
match fn {
copy => |ptr| glib::gobject_ffi::g_boxed_copy(ffi::gst_rtsp_auth_credential_get_type(), ptr as *mut _) as *mut ffi::GstRTSPAuthCredential,
free => |ptr| glib::gobject_ffi::g_boxed_free(ffi::gst_rtsp_auth_credential_get_type(), ptr as *mut _),
type_ => || ffi::gst_rtsp_auth_credential_get_type(),
}
}
unsafe impl Send for RTSPAuthCredential {}

View file

@ -27,6 +27,9 @@ pub use crate::auto::*;
#[cfg(feature = "serde")]
mod flag_serde;
pub mod rtsp_auth_credential;
pub mod rtsp_message;
// Re-export all the traits in a prelude module, so that applications
// can always "use gst_rtsp::prelude::*" without getting conflicts
pub mod prelude {

View file

@ -0,0 +1,26 @@
use crate::{RTSPAuthCredential, RTSPAuthMethod, RTSPAuthParam};
use ffi::GstRTSPAuthCredential;
use glib::translate::*;
impl RTSPAuthCredential {
pub fn scheme(&self) -> RTSPAuthMethod {
let ptr: *mut GstRTSPAuthCredential = self.to_glib_none().0;
unsafe { from_glib((*ptr).scheme) }
}
pub fn authorization(&self) -> Option<&str> {
let ptr: *mut GstRTSPAuthCredential = self.to_glib_none().0;
unsafe {
if (*ptr).authorization.is_null() {
None
} else {
std::ffi::CStr::from_ptr((*ptr).authorization).to_str().ok()
}
}
}
pub fn params(&self) -> glib::collections::PtrSlice<RTSPAuthParam> {
let ptr: *mut GstRTSPAuthCredential = self.to_glib_none().0;
unsafe { FromGlibPtrContainer::from_glib_none((*ptr).params) }
}
}

View file

@ -0,0 +1,25 @@
use glib::translate::*;
impl RTSPAuthParam {
pub fn name(&self) -> Option<&str> {
let ptr: *mut GstRTSPAuthParam = self.to_glib_none().0;
unsafe {
if (*ptr).name.is_null() {
None
} else {
std::ffi::CStr::from_ptr((*ptr).name).to_str().ok()
}
}
}
pub fn value(&self) -> Option<&str> {
let ptr: *mut GstRTSPAuthParam = self.to_glib_none().0;
unsafe {
if (*ptr).value.is_null() {
None
} else {
std::ffi::CStr::from_ptr((*ptr).value).to_str().ok()
}
}
}
}

View file

@ -0,0 +1,58 @@
use crate::{RTSPAuthCredential, RTSPHeaderField, RTSPStatusCode};
use glib::translate::*;
glib::wrapper! {
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)]
#[doc(alias = "GstRTSPMessage")]
pub struct RTSPMessage(Boxed<ffi::GstRTSPMessage>);
match fn {
copy => |ptr| {
let mut copy = std::ptr::null_mut();
let res = ffi::gst_rtsp_message_copy(ptr, &mut copy);
debug_assert_eq!(res, ffi::GST_RTSP_OK);
copy
},
free => |ptr| {
let res = ffi::gst_rtsp_message_free(ptr);
debug_assert_eq!(res, ffi::GST_RTSP_OK);
},
type_ => || ffi::gst_rtsp_msg_get_type(),
}
}
impl RTSPMessage {
pub const NONE: Option<&'static RTSPMessage> = None;
#[doc(alias = "gst_rtsp_message_add_header")]
pub fn add_header(&self, header: RTSPHeaderField, value: &str) {
let ptr = self.to_glib_none().0;
unsafe {
ffi::gst_rtsp_message_add_header(ptr, header.into_glib(), value.to_glib_none().0);
}
}
#[doc(alias = "gst_rtsp_message_init_response")]
pub fn init_response(&self, code: RTSPStatusCode, request: Option<&RTSPMessage>) {
let ptr = self.to_glib_none().0;
unsafe {
ffi::gst_rtsp_message_init_response(
ptr,
code.into_glib(),
ffi::gst_rtsp_status_as_text(code.into_glib()),
request.to_glib_none().0,
);
}
}
#[doc(alias = "gst_rtsp_message_parse_auth_credentials")]
pub fn parse_auth_credentials(&self) -> glib::collections::PtrSlice<RTSPAuthCredential> {
unsafe {
let credentials = ffi::gst_rtsp_message_parse_auth_credentials(
self.to_glib_none().0,
ffi::GST_RTSP_HDR_AUTHORIZATION,
);
FromGlibPtrContainer::from_glib_full(credentials)
}
}
}