mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-11-26 19:51:11 +00:00
ClientState -> Context
Rename the clientstate to context and put the code in a separate file.
This commit is contained in:
parent
25547176be
commit
f78a65379c
14 changed files with 472 additions and 418 deletions
|
@ -54,7 +54,7 @@ static void gst_rtsp_cgroup_pool_finalize (GObject * obj);
|
|||
static void default_thread_enter (GstRTSPThreadPool * pool,
|
||||
GstRTSPThread * thread);
|
||||
static void default_configure_thread (GstRTSPThreadPool * pool,
|
||||
GstRTSPThread * thread, GstRTSPClientState * state);
|
||||
GstRTSPThread * thread, GstRTSPContext * ctx);
|
||||
|
||||
G_DEFINE_TYPE (GstRTSPCGroupPool, gst_rtsp_cgroup_pool,
|
||||
GST_TYPE_RTSP_THREAD_POOL);
|
||||
|
@ -118,14 +118,14 @@ default_thread_enter (GstRTSPThreadPool * pool, GstRTSPThread * thread)
|
|||
|
||||
static void
|
||||
default_configure_thread (GstRTSPThreadPool * pool,
|
||||
GstRTSPThread * thread, GstRTSPClientState * state)
|
||||
GstRTSPThread * thread, GstRTSPContext * ctx)
|
||||
{
|
||||
GstRTSPCGroupPool *cpool = GST_RTSP_CGROUP_POOL (pool);
|
||||
const gchar *cls;
|
||||
struct cgroup *cgroup;
|
||||
|
||||
if (state->token)
|
||||
cls = gst_rtsp_token_get_string (state->token, "cgroup.pool.media.class");
|
||||
if (ctx->token)
|
||||
cls = gst_rtsp_token_get_string (ctx->token, "cgroup.pool.media.class");
|
||||
else
|
||||
cls = NULL;
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
public_headers = \
|
||||
rtsp-auth.h \
|
||||
rtsp-address-pool.h \
|
||||
rtsp-context.h \
|
||||
rtsp-params.h \
|
||||
rtsp-sdp.h \
|
||||
rtsp-thread-pool.h \
|
||||
|
@ -21,6 +22,7 @@ public_headers = \
|
|||
c_sources = \
|
||||
rtsp-auth.c \
|
||||
rtsp-address-pool.c \
|
||||
rtsp-context.c \
|
||||
rtsp-params.c \
|
||||
rtsp-sdp.c \
|
||||
rtsp-thread-pool.c \
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
* The RTSP server will call gst_rtsp_auth_check() with a string describing the
|
||||
* check to perform. The possible checks are prefixed with
|
||||
* #GST_RTSP_AUTH_CHECK_*. Depending on the check, the default implementation
|
||||
* will use the current #GstRTSPToken, #GstRTSPClientState and
|
||||
* will use the current #GstRTSPToken, #GstRTSPContext and
|
||||
* #GstRTSPPermissions on the object to check if an operation is allowed.
|
||||
*
|
||||
* The default #GstRTSPAuth object has support for basic authentication. With
|
||||
|
@ -79,9 +79,8 @@ static void gst_rtsp_auth_set_property (GObject * object, guint propid,
|
|||
const GValue * value, GParamSpec * pspec);
|
||||
static void gst_rtsp_auth_finalize (GObject * obj);
|
||||
|
||||
static gboolean default_authenticate (GstRTSPAuth * auth,
|
||||
GstRTSPClientState * state);
|
||||
static gboolean default_check (GstRTSPAuth * auth, GstRTSPClientState * state,
|
||||
static gboolean default_authenticate (GstRTSPAuth * auth, GstRTSPContext * ctx);
|
||||
static gboolean default_check (GstRTSPAuth * auth, GstRTSPContext * ctx,
|
||||
const gchar * check);
|
||||
|
||||
G_DEFINE_TYPE (GstRTSPAuth, gst_rtsp_auth, G_TYPE_OBJECT);
|
||||
|
@ -340,7 +339,7 @@ gst_rtsp_auth_remove_basic (GstRTSPAuth * auth, const gchar * basic)
|
|||
}
|
||||
|
||||
static gboolean
|
||||
default_authenticate (GstRTSPAuth * auth, GstRTSPClientState * state)
|
||||
default_authenticate (GstRTSPAuth * auth, GstRTSPContext * ctx)
|
||||
{
|
||||
GstRTSPAuthPrivate *priv = auth->priv;
|
||||
GstRTSPResult res;
|
||||
|
@ -349,13 +348,13 @@ default_authenticate (GstRTSPAuth * auth, GstRTSPClientState * state)
|
|||
GST_DEBUG_OBJECT (auth, "authenticate");
|
||||
|
||||
g_mutex_lock (&priv->lock);
|
||||
/* FIXME, need to ref but we have no way to unref when the state is
|
||||
/* FIXME, need to ref but we have no way to unref when the ctx is
|
||||
* popped */
|
||||
state->token = priv->default_token;
|
||||
ctx->token = priv->default_token;
|
||||
g_mutex_unlock (&priv->lock);
|
||||
|
||||
res =
|
||||
gst_rtsp_message_get_header (state->request, GST_RTSP_HDR_AUTHORIZATION,
|
||||
gst_rtsp_message_get_header (ctx->request, GST_RTSP_HDR_AUTHORIZATION,
|
||||
&authorization, 0);
|
||||
if (res < 0)
|
||||
goto no_auth;
|
||||
|
@ -368,7 +367,7 @@ default_authenticate (GstRTSPAuth * auth, GstRTSPClientState * state)
|
|||
g_mutex_lock (&priv->lock);
|
||||
if ((token = g_hash_table_lookup (priv->basic, &authorization[6]))) {
|
||||
GST_DEBUG_OBJECT (auth, "setting token %p", token);
|
||||
state->token = token;
|
||||
ctx->token = token;
|
||||
}
|
||||
g_mutex_unlock (&priv->lock);
|
||||
} else if (g_ascii_strncasecmp (authorization, "digest ", 7) == 0) {
|
||||
|
@ -385,35 +384,34 @@ no_auth:
|
|||
}
|
||||
|
||||
static void
|
||||
send_response (GstRTSPAuth * auth, GstRTSPStatusCode code,
|
||||
GstRTSPClientState * state)
|
||||
send_response (GstRTSPAuth * auth, GstRTSPStatusCode code, GstRTSPContext * ctx)
|
||||
{
|
||||
gst_rtsp_message_init_response (state->response, code,
|
||||
gst_rtsp_status_as_text (code), state->request);
|
||||
gst_rtsp_message_init_response (ctx->response, code,
|
||||
gst_rtsp_status_as_text (code), ctx->request);
|
||||
|
||||
if (code == GST_RTSP_STS_UNAUTHORIZED) {
|
||||
/* we only have Basic for now */
|
||||
gst_rtsp_message_add_header (state->response, GST_RTSP_HDR_WWW_AUTHENTICATE,
|
||||
gst_rtsp_message_add_header (ctx->response, GST_RTSP_HDR_WWW_AUTHENTICATE,
|
||||
"Basic realm=\"GStreamer RTSP Server\"");
|
||||
}
|
||||
gst_rtsp_client_send_message (state->client, state->session, state->response);
|
||||
gst_rtsp_client_send_message (ctx->client, ctx->session, ctx->response);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
ensure_authenticated (GstRTSPAuth * auth, GstRTSPClientState * state)
|
||||
ensure_authenticated (GstRTSPAuth * auth, GstRTSPContext * ctx)
|
||||
{
|
||||
GstRTSPAuthClass *klass;
|
||||
|
||||
klass = GST_RTSP_AUTH_GET_CLASS (auth);
|
||||
|
||||
/* we need a token to check */
|
||||
if (state->token == NULL) {
|
||||
if (ctx->token == NULL) {
|
||||
if (klass->authenticate) {
|
||||
if (!klass->authenticate (auth, state))
|
||||
if (!klass->authenticate (auth, ctx))
|
||||
goto authenticate_failed;
|
||||
}
|
||||
}
|
||||
if (state->token == NULL)
|
||||
if (ctx->token == NULL)
|
||||
goto no_auth;
|
||||
|
||||
return TRUE;
|
||||
|
@ -422,21 +420,20 @@ ensure_authenticated (GstRTSPAuth * auth, GstRTSPClientState * state)
|
|||
authenticate_failed:
|
||||
{
|
||||
GST_DEBUG_OBJECT (auth, "authenticate failed");
|
||||
send_response (auth, GST_RTSP_STS_UNAUTHORIZED, state);
|
||||
send_response (auth, GST_RTSP_STS_UNAUTHORIZED, ctx);
|
||||
return FALSE;
|
||||
}
|
||||
no_auth:
|
||||
{
|
||||
GST_DEBUG_OBJECT (auth, "no authorization token found");
|
||||
send_response (auth, GST_RTSP_STS_UNAUTHORIZED, state);
|
||||
send_response (auth, GST_RTSP_STS_UNAUTHORIZED, ctx);
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
/* new connection */
|
||||
static gboolean
|
||||
check_connect (GstRTSPAuth * auth, GstRTSPClientState * state,
|
||||
const gchar * check)
|
||||
check_connect (GstRTSPAuth * auth, GstRTSPContext * ctx, const gchar * check)
|
||||
{
|
||||
GstRTSPAuthPrivate *priv = auth->priv;
|
||||
|
||||
|
@ -444,7 +441,7 @@ check_connect (GstRTSPAuth * auth, GstRTSPClientState * state,
|
|||
GTlsConnection *tls;
|
||||
|
||||
/* configure the connection */
|
||||
tls = gst_rtsp_connection_get_tls (state->conn, NULL);
|
||||
tls = gst_rtsp_connection_get_tls (ctx->conn, NULL);
|
||||
g_tls_connection_set_certificate (tls, priv->certificate);
|
||||
}
|
||||
return TRUE;
|
||||
|
@ -452,12 +449,12 @@ check_connect (GstRTSPAuth * auth, GstRTSPClientState * state,
|
|||
|
||||
/* check url and methods */
|
||||
static gboolean
|
||||
check_url (GstRTSPAuth * auth, GstRTSPClientState * state, const gchar * check)
|
||||
check_url (GstRTSPAuth * auth, GstRTSPContext * ctx, const gchar * check)
|
||||
{
|
||||
GstRTSPAuthPrivate *priv = auth->priv;
|
||||
|
||||
if ((state->method & priv->methods) != 0)
|
||||
if (!ensure_authenticated (auth, state))
|
||||
if ((ctx->method & priv->methods) != 0)
|
||||
if (!ensure_authenticated (auth, ctx))
|
||||
goto not_authenticated;
|
||||
|
||||
return TRUE;
|
||||
|
@ -471,19 +468,18 @@ not_authenticated:
|
|||
|
||||
/* check access to media factory */
|
||||
static gboolean
|
||||
check_factory (GstRTSPAuth * auth, GstRTSPClientState * state,
|
||||
const gchar * check)
|
||||
check_factory (GstRTSPAuth * auth, GstRTSPContext * ctx, const gchar * check)
|
||||
{
|
||||
const gchar *role;
|
||||
GstRTSPPermissions *perms;
|
||||
|
||||
if (!ensure_authenticated (auth, state))
|
||||
if (!ensure_authenticated (auth, ctx))
|
||||
return FALSE;
|
||||
|
||||
if (!(role = gst_rtsp_token_get_string (state->token,
|
||||
if (!(role = gst_rtsp_token_get_string (ctx->token,
|
||||
GST_RTSP_TOKEN_MEDIA_FACTORY_ROLE)))
|
||||
goto no_media_role;
|
||||
if (!(perms = gst_rtsp_media_factory_get_permissions (state->factory)))
|
||||
if (!(perms = gst_rtsp_media_factory_get_permissions (ctx->factory)))
|
||||
goto no_permissions;
|
||||
|
||||
if (g_str_equal (check, GST_RTSP_AUTH_CHECK_MEDIA_FACTORY_ACCESS)) {
|
||||
|
@ -501,55 +497,54 @@ check_factory (GstRTSPAuth * auth, GstRTSPClientState * state,
|
|||
no_media_role:
|
||||
{
|
||||
GST_DEBUG_OBJECT (auth, "no media factory role found");
|
||||
send_response (auth, GST_RTSP_STS_UNAUTHORIZED, state);
|
||||
send_response (auth, GST_RTSP_STS_UNAUTHORIZED, ctx);
|
||||
return FALSE;
|
||||
}
|
||||
no_permissions:
|
||||
{
|
||||
GST_DEBUG_OBJECT (auth, "no permissions on media factory found");
|
||||
send_response (auth, GST_RTSP_STS_UNAUTHORIZED, state);
|
||||
send_response (auth, GST_RTSP_STS_UNAUTHORIZED, ctx);
|
||||
return FALSE;
|
||||
}
|
||||
no_access:
|
||||
{
|
||||
GST_DEBUG_OBJECT (auth, "no permissions to access media factory");
|
||||
send_response (auth, GST_RTSP_STS_NOT_FOUND, state);
|
||||
send_response (auth, GST_RTSP_STS_NOT_FOUND, ctx);
|
||||
return FALSE;
|
||||
}
|
||||
no_construct:
|
||||
{
|
||||
GST_DEBUG_OBJECT (auth, "no permissions to construct media factory");
|
||||
send_response (auth, GST_RTSP_STS_UNAUTHORIZED, state);
|
||||
send_response (auth, GST_RTSP_STS_UNAUTHORIZED, ctx);
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
static gboolean
|
||||
check_client_settings (GstRTSPAuth * auth, GstRTSPClientState * state,
|
||||
check_client_settings (GstRTSPAuth * auth, GstRTSPContext * ctx,
|
||||
const gchar * check)
|
||||
{
|
||||
if (!ensure_authenticated (auth, state))
|
||||
if (!ensure_authenticated (auth, ctx))
|
||||
return FALSE;
|
||||
|
||||
return gst_rtsp_token_is_allowed (state->token,
|
||||
return gst_rtsp_token_is_allowed (ctx->token,
|
||||
GST_RTSP_TOKEN_TRANSPORT_CLIENT_SETTINGS);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
default_check (GstRTSPAuth * auth, GstRTSPClientState * state,
|
||||
const gchar * check)
|
||||
default_check (GstRTSPAuth * auth, GstRTSPContext * ctx, const gchar * check)
|
||||
{
|
||||
gboolean res = FALSE;
|
||||
|
||||
/* FIXME, use hastable or so */
|
||||
if (g_str_equal (check, GST_RTSP_AUTH_CHECK_CONNECT)) {
|
||||
res = check_connect (auth, state, check);
|
||||
res = check_connect (auth, ctx, check);
|
||||
} else if (g_str_equal (check, GST_RTSP_AUTH_CHECK_URL)) {
|
||||
res = check_url (auth, state, check);
|
||||
res = check_url (auth, ctx, check);
|
||||
} else if (g_str_has_prefix (check, "auth.check.media.factory.")) {
|
||||
res = check_factory (auth, state, check);
|
||||
res = check_factory (auth, ctx, check);
|
||||
} else if (g_str_equal (check, GST_RTSP_AUTH_CHECK_TRANSPORT_CLIENT_SETTINGS)) {
|
||||
res = check_client_settings (auth, state, check);
|
||||
res = check_client_settings (auth, ctx, check);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
@ -580,16 +575,16 @@ gst_rtsp_auth_check (const gchar * check)
|
|||
{
|
||||
gboolean result = FALSE;
|
||||
GstRTSPAuthClass *klass;
|
||||
GstRTSPClientState *state;
|
||||
GstRTSPContext *ctx;
|
||||
GstRTSPAuth *auth;
|
||||
|
||||
g_return_val_if_fail (check != NULL, FALSE);
|
||||
|
||||
if (!(state = gst_rtsp_client_state_get_current ()))
|
||||
goto no_state;
|
||||
if (!(ctx = gst_rtsp_context_get_current ()))
|
||||
goto no_context;
|
||||
|
||||
/* no auth, we don't need to check */
|
||||
if (!(auth = state->auth))
|
||||
if (!(auth = ctx->auth))
|
||||
return no_auth_check (check);
|
||||
|
||||
klass = GST_RTSP_AUTH_GET_CLASS (auth);
|
||||
|
@ -597,14 +592,14 @@ gst_rtsp_auth_check (const gchar * check)
|
|||
GST_DEBUG_OBJECT (auth, "check authorization '%s'", check);
|
||||
|
||||
if (klass->check)
|
||||
result = klass->check (auth, state, check);
|
||||
result = klass->check (auth, ctx, check);
|
||||
|
||||
return result;
|
||||
|
||||
/* ERRORS */
|
||||
no_state:
|
||||
no_context:
|
||||
{
|
||||
GST_ERROR ("no clientstate found");
|
||||
GST_ERROR ("no context found");
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,7 +56,7 @@ struct _GstRTSPAuth {
|
|||
* @authenticate: check the authentication of a client. The default implementation
|
||||
* checks if the authentication in the header matches one of the basic
|
||||
* authentication tokens. This function should set the authgroup field
|
||||
* in the state.
|
||||
* in the context.
|
||||
* @check: check if a resource can be accessed. this function should
|
||||
* call authenticate to authenticate the client when needed. The method
|
||||
* should also construct and send an appropriate response message on
|
||||
|
@ -67,8 +67,8 @@ struct _GstRTSPAuth {
|
|||
struct _GstRTSPAuthClass {
|
||||
GObjectClass parent_class;
|
||||
|
||||
gboolean (*authenticate) (GstRTSPAuth *auth, GstRTSPClientState *state);
|
||||
gboolean (*check) (GstRTSPAuth *auth, GstRTSPClientState *state,
|
||||
gboolean (*authenticate) (GstRTSPAuth *auth, GstRTSPContext *ctx);
|
||||
gboolean (*check) (GstRTSPAuth *auth, GstRTSPContext *ctx,
|
||||
const gchar *check);
|
||||
};
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -27,18 +27,12 @@ G_BEGIN_DECLS
|
|||
|
||||
typedef struct _GstRTSPClient GstRTSPClient;
|
||||
typedef struct _GstRTSPClientClass GstRTSPClientClass;
|
||||
typedef struct _GstRTSPClientState GstRTSPClientState;
|
||||
typedef struct _GstRTSPClientPrivate GstRTSPClientPrivate;
|
||||
|
||||
#include "rtsp-server.h"
|
||||
#include "rtsp-media.h"
|
||||
#include "rtsp-context.h"
|
||||
#include "rtsp-mount-points.h"
|
||||
#include "rtsp-session-pool.h"
|
||||
#include "rtsp-session-media.h"
|
||||
#include "rtsp-auth.h"
|
||||
#include "rtsp-thread-pool.h"
|
||||
#include "rtsp-token.h"
|
||||
#include "rtsp-sdp.h"
|
||||
#include "rtsp-auth.h"
|
||||
|
||||
#define GST_TYPE_RTSP_CLIENT (gst_rtsp_client_get_type ())
|
||||
#define GST_IS_RTSP_CLIENT(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GST_TYPE_RTSP_CLIENT))
|
||||
|
@ -49,47 +43,6 @@ typedef struct _GstRTSPClientPrivate GstRTSPClientPrivate;
|
|||
#define GST_RTSP_CLIENT_CAST(obj) ((GstRTSPClient*)(obj))
|
||||
#define GST_RTSP_CLIENT_CLASS_CAST(klass) ((GstRTSPClientClass*)(klass))
|
||||
|
||||
/**
|
||||
* GstRTSPClientState:
|
||||
* @server: the server
|
||||
* @conn: the connection
|
||||
* @client: the client
|
||||
* @request: the complete request
|
||||
* @uri: the complete url parsed from @request
|
||||
* @method: the parsed method of @uri
|
||||
* @auth: the current auth object or NULL
|
||||
* @token: authorisation token
|
||||
* @session: the session, can be NULL
|
||||
* @sessmedia: the session media for the url can be NULL
|
||||
* @factory: the media factory for the url, can be NULL.
|
||||
* @media: the media for the url can be NULL
|
||||
* @stream: the stream for the url can be NULL
|
||||
* @response: the response
|
||||
*
|
||||
* Information passed around containing the client state of a request.
|
||||
*/
|
||||
struct _GstRTSPClientState {
|
||||
GstRTSPServer *server;
|
||||
GstRTSPConnection *conn;
|
||||
GstRTSPClient *client;
|
||||
GstRTSPMessage *request;
|
||||
GstRTSPUrl *uri;
|
||||
GstRTSPMethod method;
|
||||
GstRTSPAuth *auth;
|
||||
GstRTSPToken *token;
|
||||
GstRTSPSession *session;
|
||||
GstRTSPSessionMedia *sessmedia;
|
||||
GstRTSPMediaFactory *factory;
|
||||
GstRTSPMedia *media;
|
||||
GstRTSPStream *stream;
|
||||
GstRTSPMessage *response;
|
||||
};
|
||||
|
||||
GstRTSPClientState * gst_rtsp_client_state_get_current (void);
|
||||
void gst_rtsp_client_state_push_current (GstRTSPClientState * state);
|
||||
void gst_rtsp_client_state_pop_current (GstRTSPClientState * state);
|
||||
|
||||
|
||||
/**
|
||||
* GstRTSPClientSendFunc:
|
||||
* @client: a #GstRTSPClient
|
||||
|
@ -124,9 +77,9 @@ struct _GstRTSPClient {
|
|||
* @configure_client_transport: called when the client transport needs to be
|
||||
* configured.
|
||||
* @params_set: set parameters. This function should also initialize the
|
||||
* RTSP response(state->response) via a call to gst_rtsp_message_init_response()
|
||||
* RTSP response(ctx->response) via a call to gst_rtsp_message_init_response()
|
||||
* @params_get: get parameters. This function should also initialize the
|
||||
* RTSP response(state->response) via a call to gst_rtsp_message_init_response()
|
||||
* RTSP response(ctx->response) via a call to gst_rtsp_message_init_response()
|
||||
*
|
||||
* The client class structure.
|
||||
*/
|
||||
|
@ -135,22 +88,22 @@ struct _GstRTSPClientClass {
|
|||
|
||||
GstSDPMessage * (*create_sdp) (GstRTSPClient *client, GstRTSPMedia *media);
|
||||
gboolean (*configure_client_transport) (GstRTSPClient * client,
|
||||
GstRTSPClientState * state,
|
||||
GstRTSPContext * ctx,
|
||||
GstRTSPTransport * ct);
|
||||
GstRTSPResult (*params_set) (GstRTSPClient *client, GstRTSPClientState *state);
|
||||
GstRTSPResult (*params_get) (GstRTSPClient *client, GstRTSPClientState *state);
|
||||
GstRTSPResult (*params_set) (GstRTSPClient *client, GstRTSPContext *ctx);
|
||||
GstRTSPResult (*params_get) (GstRTSPClient *client, GstRTSPContext *ctx);
|
||||
|
||||
/* signals */
|
||||
void (*closed) (GstRTSPClient *client);
|
||||
void (*new_session) (GstRTSPClient *client, GstRTSPSession *session);
|
||||
void (*options_request) (GstRTSPClient *client, GstRTSPClientState *state);
|
||||
void (*describe_request) (GstRTSPClient *client, GstRTSPClientState *state);
|
||||
void (*setup_request) (GstRTSPClient *client, GstRTSPClientState *state);
|
||||
void (*play_request) (GstRTSPClient *client, GstRTSPClientState *state);
|
||||
void (*pause_request) (GstRTSPClient *client, GstRTSPClientState *state);
|
||||
void (*teardown_request) (GstRTSPClient *client, GstRTSPClientState *state);
|
||||
void (*set_parameter_request) (GstRTSPClient *client, GstRTSPClientState *state);
|
||||
void (*get_parameter_request) (GstRTSPClient *client, GstRTSPClientState *state);
|
||||
void (*options_request) (GstRTSPClient *client, GstRTSPContext *ctx);
|
||||
void (*describe_request) (GstRTSPClient *client, GstRTSPContext *ctx);
|
||||
void (*setup_request) (GstRTSPClient *client, GstRTSPContext *ctx);
|
||||
void (*play_request) (GstRTSPClient *client, GstRTSPContext *ctx);
|
||||
void (*pause_request) (GstRTSPClient *client, GstRTSPContext *ctx);
|
||||
void (*teardown_request) (GstRTSPClient *client, GstRTSPContext *ctx);
|
||||
void (*set_parameter_request) (GstRTSPClient *client, GstRTSPContext *ctx);
|
||||
void (*get_parameter_request) (GstRTSPClient *client, GstRTSPContext *ctx);
|
||||
};
|
||||
|
||||
GType gst_rtsp_client_get_type (void);
|
||||
|
|
90
gst/rtsp-server/rtsp-context.c
Normal file
90
gst/rtsp-server/rtsp-context.c
Normal file
|
@ -0,0 +1,90 @@
|
|||
/* GStreamer
|
||||
* Copyright (C) 2013 Wim Taymans <wim.taymans at gmail.com>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Library General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Library General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Library General Public
|
||||
* License along with this library; if not, write to the
|
||||
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
/**
|
||||
* SECTION:rtsp-context
|
||||
* @short_description: A client request context
|
||||
* @see_also: #GstRTSPServer, #GstRTSPClient
|
||||
*
|
||||
* Last reviewed on 2013-07-11 (1.0.0)
|
||||
*/
|
||||
|
||||
#include "rtsp-context.h"
|
||||
|
||||
static GPrivate current_context;
|
||||
|
||||
/**
|
||||
* gst_rtsp_context_get_current:
|
||||
*
|
||||
* Get the current #GstRTSPContext. This object is retrieved from the
|
||||
* current thread that is handling the request for a client.
|
||||
*
|
||||
* Returns: a #GstRTSPContext
|
||||
*/
|
||||
GstRTSPContext *
|
||||
gst_rtsp_context_get_current (void)
|
||||
{
|
||||
GSList *l;
|
||||
|
||||
l = g_private_get (¤t_context);
|
||||
if (l == NULL)
|
||||
return NULL;
|
||||
|
||||
return (GstRTSPContext *) (l->data);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* gst_rtsp_context_push_current:
|
||||
* @ctx: a ##GstRTSPContext
|
||||
*
|
||||
* Pushes @ctx onto the context stack. The current
|
||||
* context can then be received using gst_rtsp_context_get_current().
|
||||
**/
|
||||
void
|
||||
gst_rtsp_context_push_current (GstRTSPContext * ctx)
|
||||
{
|
||||
GSList *l;
|
||||
|
||||
g_return_if_fail (ctx != NULL);
|
||||
|
||||
l = g_private_get (¤t_context);
|
||||
l = g_slist_prepend (l, ctx);
|
||||
g_private_set (¤t_context, l);
|
||||
}
|
||||
|
||||
/**
|
||||
* gst_rtsp_context_pop_current:
|
||||
* @ctx: a #GstRTSPContext
|
||||
*
|
||||
* Pops @ctx off the context stack (verifying that @ctx
|
||||
* is on the top of the stack).
|
||||
**/
|
||||
void
|
||||
gst_rtsp_context_pop_current (GstRTSPContext * ctx)
|
||||
{
|
||||
GSList *l;
|
||||
|
||||
l = g_private_get (¤t_context);
|
||||
|
||||
g_return_if_fail (l != NULL);
|
||||
g_return_if_fail (l->data == ctx);
|
||||
|
||||
l = g_slist_delete_link (l, l);
|
||||
g_private_set (¤t_context, l);
|
||||
}
|
81
gst/rtsp-server/rtsp-context.h
Normal file
81
gst/rtsp-server/rtsp-context.h
Normal file
|
@ -0,0 +1,81 @@
|
|||
/* GStreamer
|
||||
* Copyright (C) 2008 Wim Taymans <wim.taymans at gmail.com>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Library General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Library General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Library General Public
|
||||
* License along with this library; if not, write to the
|
||||
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include <gst/gst.h>
|
||||
#include <gst/rtsp/gstrtspconnection.h>
|
||||
|
||||
#ifndef __GST_RTSP_CONTEXT_H__
|
||||
#define __GST_RTSP_CONTEXT_H__
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
typedef struct _GstRTSPContext GstRTSPContext;
|
||||
|
||||
#include "rtsp-server.h"
|
||||
#include "rtsp-media.h"
|
||||
#include "rtsp-media-factory.h"
|
||||
#include "rtsp-session-media.h"
|
||||
#include "rtsp-auth.h"
|
||||
#include "rtsp-thread-pool.h"
|
||||
#include "rtsp-token.h"
|
||||
|
||||
/**
|
||||
* GstRTSPContext:
|
||||
* @server: the server
|
||||
* @conn: the connection
|
||||
* @client: the client
|
||||
* @request: the complete request
|
||||
* @uri: the complete url parsed from @request
|
||||
* @method: the parsed method of @uri
|
||||
* @auth: the current auth object or NULL
|
||||
* @token: authorisation token
|
||||
* @session: the session, can be NULL
|
||||
* @sessmedia: the session media for the url can be NULL
|
||||
* @factory: the media factory for the url, can be NULL.
|
||||
* @media: the media for the url can be NULL
|
||||
* @stream: the stream for the url can be NULL
|
||||
* @response: the response
|
||||
*
|
||||
* Information passed around containing the context of a request.
|
||||
*/
|
||||
struct _GstRTSPContext {
|
||||
GstRTSPServer *server;
|
||||
GstRTSPConnection *conn;
|
||||
GstRTSPClient *client;
|
||||
GstRTSPMessage *request;
|
||||
GstRTSPUrl *uri;
|
||||
GstRTSPMethod method;
|
||||
GstRTSPAuth *auth;
|
||||
GstRTSPToken *token;
|
||||
GstRTSPSession *session;
|
||||
GstRTSPSessionMedia *sessmedia;
|
||||
GstRTSPMediaFactory *factory;
|
||||
GstRTSPMedia *media;
|
||||
GstRTSPStream *stream;
|
||||
GstRTSPMessage *response;
|
||||
};
|
||||
|
||||
GstRTSPContext * gst_rtsp_context_get_current (void);
|
||||
void gst_rtsp_context_push_current (GstRTSPContext * ctx);
|
||||
void gst_rtsp_context_pop_current (GstRTSPContext * ctx);
|
||||
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif /* __GST_RTSP_CONTEXT_H__ */
|
|
@ -31,14 +31,14 @@
|
|||
/**
|
||||
* gst_rtsp_params_set:
|
||||
* @client: a #GstRTSPClient
|
||||
* @state: a #GstRTSPClientState
|
||||
* @ctx: a #GstRTSPContext
|
||||
*
|
||||
* Set parameters (not implemented yet)
|
||||
*
|
||||
* Returns: a #GstRTSPResult
|
||||
*/
|
||||
GstRTSPResult
|
||||
gst_rtsp_params_set (GstRTSPClient * client, GstRTSPClientState * state)
|
||||
gst_rtsp_params_set (GstRTSPClient * client, GstRTSPContext * ctx)
|
||||
{
|
||||
GstRTSPStatusCode code;
|
||||
|
||||
|
@ -46,8 +46,8 @@ gst_rtsp_params_set (GstRTSPClient * client, GstRTSPClientState * state)
|
|||
* with a list of the parameters */
|
||||
code = GST_RTSP_STS_PARAMETER_NOT_UNDERSTOOD;
|
||||
|
||||
gst_rtsp_message_init_response (state->response, code,
|
||||
gst_rtsp_status_as_text (code), state->request);
|
||||
gst_rtsp_message_init_response (ctx->response, code,
|
||||
gst_rtsp_status_as_text (code), ctx->request);
|
||||
|
||||
return GST_RTSP_OK;
|
||||
}
|
||||
|
@ -55,14 +55,14 @@ gst_rtsp_params_set (GstRTSPClient * client, GstRTSPClientState * state)
|
|||
/**
|
||||
* gst_rtsp_params_get:
|
||||
* @client: a #GstRTSPClient
|
||||
* @state: a #GstRTSPClientState
|
||||
* @ctx: a #GstRTSPContext
|
||||
*
|
||||
* Get parameters (not implemented yet)
|
||||
*
|
||||
* Returns: a #GstRTSPResult
|
||||
*/
|
||||
GstRTSPResult
|
||||
gst_rtsp_params_get (GstRTSPClient * client, GstRTSPClientState * state)
|
||||
gst_rtsp_params_get (GstRTSPClient * client, GstRTSPContext * ctx)
|
||||
{
|
||||
GstRTSPStatusCode code;
|
||||
|
||||
|
@ -70,8 +70,8 @@ gst_rtsp_params_get (GstRTSPClient * client, GstRTSPClientState * state)
|
|||
* with a list of the parameters */
|
||||
code = GST_RTSP_STS_PARAMETER_NOT_UNDERSTOOD;
|
||||
|
||||
gst_rtsp_message_init_response (state->response, code,
|
||||
gst_rtsp_status_as_text (code), state->request);
|
||||
gst_rtsp_message_init_response (ctx->response, code,
|
||||
gst_rtsp_status_as_text (code), ctx->request);
|
||||
|
||||
return GST_RTSP_OK;
|
||||
}
|
||||
|
|
|
@ -30,8 +30,8 @@
|
|||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
GstRTSPResult gst_rtsp_params_set (GstRTSPClient * client, GstRTSPClientState * state);
|
||||
GstRTSPResult gst_rtsp_params_get (GstRTSPClient * client, GstRTSPClientState * state);
|
||||
GstRTSPResult gst_rtsp_params_set (GstRTSPClient * client, GstRTSPContext * ctx);
|
||||
GstRTSPResult gst_rtsp_params_get (GstRTSPClient * client, GstRTSPContext * ctx);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
|
|
|
@ -991,26 +991,26 @@ unmanage_client (GstRTSPClient * client, ClientContext * ctx)
|
|||
static void
|
||||
manage_client (GstRTSPServer * server, GstRTSPClient * client)
|
||||
{
|
||||
ClientContext *ctx;
|
||||
ClientContext *cctx;
|
||||
GstRTSPServerPrivate *priv = server->priv;
|
||||
GMainContext *mainctx = NULL;
|
||||
GstRTSPClientState state = { NULL };
|
||||
GstRTSPContext ctx = { NULL };
|
||||
|
||||
GST_DEBUG_OBJECT (server, "manage client %p", client);
|
||||
|
||||
ctx = g_slice_new0 (ClientContext);
|
||||
ctx->server = g_object_ref (server);
|
||||
ctx->client = client;
|
||||
cctx = g_slice_new0 (ClientContext);
|
||||
cctx->server = g_object_ref (server);
|
||||
cctx->client = client;
|
||||
|
||||
GST_RTSP_SERVER_LOCK (server);
|
||||
|
||||
state.server = server;
|
||||
state.client = client;
|
||||
ctx.server = server;
|
||||
ctx.client = client;
|
||||
|
||||
ctx->thread = gst_rtsp_thread_pool_get_thread (priv->thread_pool,
|
||||
GST_RTSP_THREAD_TYPE_CLIENT, &state);
|
||||
if (ctx->thread)
|
||||
mainctx = ctx->thread->context;
|
||||
cctx->thread = gst_rtsp_thread_pool_get_thread (priv->thread_pool,
|
||||
GST_RTSP_THREAD_TYPE_CLIENT, &ctx);
|
||||
if (cctx->thread)
|
||||
mainctx = cctx->thread->context;
|
||||
else {
|
||||
GSource *source;
|
||||
/* find the context to add the watch */
|
||||
|
@ -1018,8 +1018,8 @@ manage_client (GstRTSPServer * server, GstRTSPClient * client)
|
|||
mainctx = g_source_get_context (source);
|
||||
}
|
||||
|
||||
g_signal_connect (client, "closed", (GCallback) unmanage_client, ctx);
|
||||
priv->clients = g_list_prepend (priv->clients, ctx);
|
||||
g_signal_connect (client, "closed", (GCallback) unmanage_client, cctx);
|
||||
priv->clients = g_list_prepend (priv->clients, cctx);
|
||||
|
||||
gst_rtsp_client_attach (client, mainctx);
|
||||
|
||||
|
@ -1129,17 +1129,17 @@ gst_rtsp_server_io_func (GSocket * socket, GIOCondition condition,
|
|||
GstRTSPServerClass *klass;
|
||||
GstRTSPResult res;
|
||||
GstRTSPConnection *conn = NULL;
|
||||
GstRTSPClientState state = { NULL };
|
||||
GstRTSPContext ctx = { NULL };
|
||||
|
||||
if (condition & G_IO_IN) {
|
||||
/* a new client connected. */
|
||||
GST_RTSP_CHECK (gst_rtsp_connection_accept (socket, &conn, NULL),
|
||||
accept_failed);
|
||||
|
||||
state.server = server;
|
||||
state.conn = conn;
|
||||
state.auth = priv->auth;
|
||||
gst_rtsp_client_state_push_current (&state);
|
||||
ctx.server = server;
|
||||
ctx.conn = conn;
|
||||
ctx.auth = priv->auth;
|
||||
gst_rtsp_context_push_current (&ctx);
|
||||
|
||||
if (!gst_rtsp_auth_check (GST_RTSP_AUTH_CHECK_CONNECT))
|
||||
goto connection_refused;
|
||||
|
@ -1163,7 +1163,7 @@ gst_rtsp_server_io_func (GSocket * socket, GIOCondition condition,
|
|||
GST_WARNING_OBJECT (server, "received unknown event %08x", condition);
|
||||
}
|
||||
exit:
|
||||
gst_rtsp_client_state_pop_current (&state);
|
||||
gst_rtsp_context_pop_current (&ctx);
|
||||
|
||||
return G_SOURCE_CONTINUE;
|
||||
|
||||
|
|
|
@ -197,7 +197,7 @@ static void gst_rtsp_thread_pool_finalize (GObject * obj);
|
|||
|
||||
static gpointer do_loop (GstRTSPThread * thread);
|
||||
static GstRTSPThread *default_get_thread (GstRTSPThreadPool * pool,
|
||||
GstRTSPThreadType type, GstRTSPClientState * state);
|
||||
GstRTSPThreadType type, GstRTSPContext * ctx);
|
||||
|
||||
G_DEFINE_TYPE (GstRTSPThreadPool, gst_rtsp_thread_pool, G_TYPE_OBJECT);
|
||||
|
||||
|
@ -393,7 +393,7 @@ gst_rtsp_thread_pool_get_max_threads (GstRTSPThreadPool * pool)
|
|||
|
||||
static GstRTSPThread *
|
||||
make_thread (GstRTSPThreadPool * pool, GstRTSPThreadType type,
|
||||
GstRTSPClientState * state)
|
||||
GstRTSPContext * ctx)
|
||||
{
|
||||
GstRTSPThreadPoolClass *klass;
|
||||
GstRTSPThread *thread;
|
||||
|
@ -407,14 +407,14 @@ make_thread (GstRTSPThreadPool * pool, GstRTSPThreadType type,
|
|||
GST_DEBUG_OBJECT (pool, "new thread %p", thread);
|
||||
|
||||
if (klass->configure_thread)
|
||||
klass->configure_thread (pool, thread, state);
|
||||
klass->configure_thread (pool, thread, ctx);
|
||||
|
||||
return thread;
|
||||
}
|
||||
|
||||
static GstRTSPThread *
|
||||
default_get_thread (GstRTSPThreadPool * pool,
|
||||
GstRTSPThreadType type, GstRTSPClientState * state)
|
||||
GstRTSPThreadType type, GstRTSPContext * ctx)
|
||||
{
|
||||
GstRTSPThreadPoolPrivate *priv = pool->priv;
|
||||
GstRTSPThreadPoolClass *klass;
|
||||
|
@ -450,7 +450,7 @@ default_get_thread (GstRTSPThreadPool * pool,
|
|||
} else {
|
||||
/* make more threads */
|
||||
GST_DEBUG_OBJECT (pool, "make new client thread");
|
||||
thread = make_thread (pool, type, state);
|
||||
thread = make_thread (pool, type, ctx);
|
||||
|
||||
if (!g_thread_pool_push (klass->pool, thread, &error))
|
||||
goto thread_error;
|
||||
|
@ -461,7 +461,7 @@ default_get_thread (GstRTSPThreadPool * pool,
|
|||
break;
|
||||
case GST_RTSP_THREAD_TYPE_MEDIA:
|
||||
GST_DEBUG_OBJECT (pool, "make new media thread");
|
||||
thread = make_thread (pool, type, state);
|
||||
thread = make_thread (pool, type, ctx);
|
||||
|
||||
if (!g_thread_pool_push (klass->pool, thread, &error))
|
||||
goto thread_error;
|
||||
|
@ -486,15 +486,15 @@ thread_error:
|
|||
* gst_rtsp_thread_pool_get_thread:
|
||||
* @pool: a #GstRTSPThreadPool
|
||||
* @type: the #GstRTSPThreadType
|
||||
* @state: a #GstRTSPClientState
|
||||
* @ctx: a #GstRTSPContext
|
||||
*
|
||||
* Get a new #GstRTSPThread for @type and @state.
|
||||
* Get a new #GstRTSPThread for @type and @ctx.
|
||||
*
|
||||
* Returns: a new #GstRTSPThread, gst_rtsp_thread_stop() after usage
|
||||
*/
|
||||
GstRTSPThread *
|
||||
gst_rtsp_thread_pool_get_thread (GstRTSPThreadPool * pool,
|
||||
GstRTSPThreadType type, GstRTSPClientState * state)
|
||||
GstRTSPThreadType type, GstRTSPContext * ctx)
|
||||
{
|
||||
GstRTSPThreadPoolClass *klass;
|
||||
GstRTSPThread *result = NULL;
|
||||
|
@ -504,7 +504,7 @@ gst_rtsp_thread_pool_get_thread (GstRTSPThreadPool * pool,
|
|||
klass = GST_RTSP_THREAD_POOL_GET_CLASS (pool);
|
||||
|
||||
if (klass->get_thread)
|
||||
result = klass->get_thread (pool, type, state);
|
||||
result = klass->get_thread (pool, type, ctx);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -148,10 +148,10 @@ struct _GstRTSPThreadPoolClass {
|
|||
|
||||
GstRTSPThread * (*get_thread) (GstRTSPThreadPool *pool,
|
||||
GstRTSPThreadType type,
|
||||
GstRTSPClientState *state);
|
||||
GstRTSPContext *ctx);
|
||||
void (*configure_thread) (GstRTSPThreadPool *pool,
|
||||
GstRTSPThread * thread,
|
||||
GstRTSPClientState *state);
|
||||
GstRTSPContext *ctx);
|
||||
|
||||
void (*thread_enter) (GstRTSPThreadPool *pool,
|
||||
GstRTSPThread *thread);
|
||||
|
@ -168,7 +168,7 @@ gint gst_rtsp_thread_pool_get_max_threads (GstRTSPThreadPool * po
|
|||
|
||||
GstRTSPThread * gst_rtsp_thread_pool_get_thread (GstRTSPThreadPool *pool,
|
||||
GstRTSPThreadType type,
|
||||
GstRTSPClientState *state);
|
||||
GstRTSPContext *ctx);
|
||||
G_END_DECLS
|
||||
|
||||
#endif /* __GST_RTSP_THREAD_POOL_H__ */
|
||||
|
|
|
@ -529,17 +529,17 @@ GST_START_TEST (test_client_multicast_invalid_transport_specific)
|
|||
GstRTSPMessage request = { 0, };
|
||||
gchar *str;
|
||||
GstRTSPSessionPool *session_pool;
|
||||
GstRTSPClientState state = { NULL };
|
||||
GstRTSPContext ctx = { NULL };
|
||||
|
||||
client = setup_multicast_client ();
|
||||
|
||||
state.client = client;
|
||||
state.auth = gst_rtsp_auth_new ();
|
||||
state.token =
|
||||
ctx.client = client;
|
||||
ctx.auth = gst_rtsp_auth_new ();
|
||||
ctx.token =
|
||||
gst_rtsp_token_new (GST_RTSP_TOKEN_TRANSPORT_CLIENT_SETTINGS,
|
||||
G_TYPE_BOOLEAN, TRUE, GST_RTSP_TOKEN_MEDIA_FACTORY_ROLE, G_TYPE_STRING,
|
||||
"user", NULL);
|
||||
gst_rtsp_client_state_push_current (&state);
|
||||
gst_rtsp_context_push_current (&ctx);
|
||||
|
||||
/* simple SETUP with a valid URI and multicast, but an invalid ip */
|
||||
fail_unless (gst_rtsp_message_init_request (&request, GST_RTSP_SETUP,
|
||||
|
@ -604,9 +604,9 @@ GST_START_TEST (test_client_multicast_invalid_transport_specific)
|
|||
|
||||
|
||||
g_object_unref (client);
|
||||
g_object_unref (state.auth);
|
||||
gst_rtsp_token_unref (state.token);
|
||||
gst_rtsp_client_state_pop_current (&state);
|
||||
g_object_unref (ctx.auth);
|
||||
gst_rtsp_token_unref (ctx.token);
|
||||
gst_rtsp_context_pop_current (&ctx);
|
||||
}
|
||||
|
||||
GST_END_TEST;
|
||||
|
@ -617,17 +617,17 @@ GST_START_TEST (test_client_multicast_transport_specific)
|
|||
GstRTSPMessage request = { 0, };
|
||||
gchar *str;
|
||||
GstRTSPSessionPool *session_pool;
|
||||
GstRTSPClientState state = { NULL };
|
||||
GstRTSPContext ctx = { NULL };
|
||||
|
||||
client = setup_multicast_client ();
|
||||
|
||||
state.client = client;
|
||||
state.auth = gst_rtsp_auth_new ();
|
||||
state.token =
|
||||
ctx.client = client;
|
||||
ctx.auth = gst_rtsp_auth_new ();
|
||||
ctx.token =
|
||||
gst_rtsp_token_new (GST_RTSP_TOKEN_TRANSPORT_CLIENT_SETTINGS,
|
||||
G_TYPE_BOOLEAN, TRUE, GST_RTSP_TOKEN_MEDIA_FACTORY_ROLE, G_TYPE_STRING,
|
||||
"user", NULL);
|
||||
gst_rtsp_client_state_push_current (&state);
|
||||
gst_rtsp_context_push_current (&ctx);
|
||||
|
||||
expected_transport = "RTP/AVP;multicast;destination=233.252.0.1;"
|
||||
"ttl=1;port=5000-5001;mode=\"PLAY\"";
|
||||
|
@ -655,9 +655,9 @@ GST_START_TEST (test_client_multicast_transport_specific)
|
|||
g_object_unref (session_pool);
|
||||
|
||||
g_object_unref (client);
|
||||
g_object_unref (state.auth);
|
||||
gst_rtsp_token_unref (state.token);
|
||||
gst_rtsp_client_state_pop_current (&state);
|
||||
g_object_unref (ctx.auth);
|
||||
gst_rtsp_token_unref (ctx.token);
|
||||
gst_rtsp_context_pop_current (&ctx);
|
||||
}
|
||||
|
||||
GST_END_TEST;
|
||||
|
|
Loading…
Reference in a new issue