Merge pull request #3537 from parvit/issue-695

Added configuring of authentication for rstp cameras
This commit is contained in:
Isaac Connor
2022-08-21 18:08:00 -04:00
committed by GitHub
16 changed files with 292 additions and 36 deletions

View File

@@ -85,6 +85,8 @@ FfmpegCamera::FfmpegCamera(
const Monitor *monitor,
const std::string &p_path,
const std::string &p_second_path,
const std::string &p_user,
const std::string &p_pass,
const std::string &p_method,
const std::string &p_options,
int p_width,
@@ -114,10 +116,13 @@ FfmpegCamera::FfmpegCamera(
),
mPath(p_path),
mSecondPath(p_second_path),
mUser(UriEncode(p_user)),
mPass(UriEncode(p_pass)),
mMethod(p_method),
mOptions(p_options),
hwaccel_name(p_hwaccel_name),
hwaccel_device(p_hwaccel_device)
hwaccel_device(p_hwaccel_device),
frameCount(0)
{
mMaskedPath = remove_authentication(mPath);
mMaskedSecondPath = remove_authentication(mSecondPath);
@@ -125,7 +130,6 @@ FfmpegCamera::FfmpegCamera(
FFMPEGInit();
}
frameCount = 0;
mCanCapture = false;
error_count = 0;
use_hwaccel = true;
@@ -276,6 +280,13 @@ int FfmpegCamera::OpenFfmpeg() {
std::string protocol = mPath.substr(0, 4);
protocol = StringToUpper(protocol);
if ( protocol == "RTSP" ) {
if( mUser.length() > 0 ) {
ret = av_dict_set(&opts, "auth_type", "basic", 0);
if (ret < 0) {
Warning("Could not set auth_type method to 'basic'");
}
}
const std::string method = Method();
if ( method == "rtpMulti" ) {
ret = av_dict_set(&opts, "rtsp_transport", "udp_multicast", 0);
@@ -308,6 +319,12 @@ int FfmpegCamera::OpenFfmpeg() {
mFormatContext->interrupt_callback.opaque = this;
mFormatContext->flags |= AVFMT_FLAG_NOBUFFER | AVFMT_FLAG_FLUSH_PACKETS;
if( mUser.length() > 0 ) {
// build the actual uri string with encoded parameters (from the user and pass fields)
mPath = StringToLower(protocol) + "://" + mUser + ":" + mPass + "@" + mMaskedPath.substr(7, std::string::npos);
Debug(1, "Rebuilt URI with encoded parameters: '%s'", mPath.c_str());
}
ret = avformat_open_input(&mFormatContext, mPath.c_str(), input_format, &opts);
if (ret != 0) {
logPrintf(Logger::ERROR + monitor->Importance(),

View File

@@ -38,6 +38,8 @@ class FfmpegCamera : public Camera {
std::string mPath;
std::string mMaskedPath;
std::string mSecondPath;
std::string mUser;
std::string mPass;
std::string mMaskedSecondPath;
std::string mMethod;
std::string mOptions;
@@ -71,8 +73,10 @@ class FfmpegCamera : public Camera {
public:
FfmpegCamera(
const Monitor *monitor,
const std::string &path,
const std::string &second_path,
const std::string &p_path,
const std::string &p_second_path,
const std::string &p_user,
const std::string &p_pass,
const std::string &p_method,
const std::string &p_options,
int p_width,

View File

@@ -104,6 +104,8 @@ void LibvlcUnlockBuffer(void* opaque, void* picture, void *const *planes) {
LibvlcCamera::LibvlcCamera(
const Monitor *monitor,
const std::string &p_path,
const std::string &p_user,
const std::string &p_pass,
const std::string &p_method,
const std::string &p_options,
int p_width,
@@ -131,6 +133,8 @@ LibvlcCamera::LibvlcCamera(
p_record_audio
),
mPath(p_path),
mUser(UriEncode(p_user)),
mPass(UriEncode(p_pass)),
mMethod(p_method),
mOptions(p_options)
{
@@ -214,6 +218,8 @@ int LibvlcCamera::PrimeCapture() {
opVect = Split(Options(), ",");
Debug(1, "Method: '%s'", Method().c_str());
// Set transport method as specified by method field, rtpUni is default
if ( Method() == "rtpMulti" )
opVect.push_back("--rtsp-mcast");
@@ -241,6 +247,17 @@ int LibvlcCamera::PrimeCapture() {
}
(*libvlc_log_set_f)(mLibvlcInstance, LibvlcCamera::log_callback, nullptr);
// recreate the path with encoded authentication info
if( mUser.length() > 0 ) {
std::string mMaskedPath = remove_authentication(mPath);
std::string protocol = StringToUpper(mPath.substr(0, 4));
if ( protocol == "RTSP" ) {
// build the actual uri string with encoded parameters (from the user and pass fields)
mPath = StringToLower(protocol) + "://" + mUser + ":" + mPass + "@" + mMaskedPath.substr(7, std::string::npos);
Debug(1, "Rebuilt URI with encoded parameters: '%s'", mPath.c_str());
}
}
mLibvlcMedia = (*libvlc_media_new_location_f)(mLibvlcInstance, mPath.c_str());
if ( mLibvlcMedia == nullptr ) {

View File

@@ -49,6 +49,8 @@ class LibvlcCamera : public Camera {
static void log_callback( void *ptr, int level, const libvlc_log_t *ctx, const char *format, va_list vargs );
protected:
std::string mPath;
std::string mUser;
std::string mPass;
std::string mMethod;
std::string mOptions;
StringVector opVect; // mOptArgV will point into opVect so it needs to hang around
@@ -62,7 +64,7 @@ protected:
libvlc_media_player_t *mLibvlcMediaPlayer;
public:
LibvlcCamera( const Monitor *monitor, const std::string &path, const std::string &p_method, const std::string &p_options, int p_width, int p_height, int p_colours, int p_brightness, int p_contrast, int p_hue, int p_colour, bool p_capture, bool p_record_audio );
LibvlcCamera( const Monitor *monitor, const std::string &path, const std::string &user,const std::string &pass, const std::string &p_method, const std::string &p_options, int p_width, int p_height, int p_colours, int p_brightness, int p_contrast, int p_hue, int p_colour, bool p_capture, bool p_record_audio );
~LibvlcCamera();
const std::string &Path() const { return mPath; }

View File

@@ -173,8 +173,8 @@ Monitor::Monitor()
//options
//host
//port
//user
//pass
user(),
pass(),
//path
//device
palette(0),
@@ -622,6 +622,8 @@ void Monitor::LoadCamera() {
host, // Host
port, // Port
path, // Path
user,
pass,
camera_width,
camera_height,
rtsp_describe,
@@ -658,6 +660,8 @@ void Monitor::LoadCamera() {
camera = zm::make_unique<FfmpegCamera>(this,
path,
second_path,
user,
pass,
method,
options,
camera_width,
@@ -695,6 +699,8 @@ void Monitor::LoadCamera() {
#if HAVE_LIBVLC
camera = zm::make_unique<LibvlcCamera>(this,
path.c_str(),
user,
pass,
method,
options,
camera_width,

View File

@@ -18,7 +18,9 @@
//
#include "zm_monitor.h"
#include <regex>
std::string escape_json_string( std::string input );
Monitor::JanusManager::JanusManager(Monitor *parent_) :
parent(parent_),
@@ -35,29 +37,19 @@ Monitor::JanusManager::JanusManager(Monitor *parent_) :
} else {
janus_endpoint = "127.0.0.1:8088/janus";
}
rtsp_username = "";
rtsp_password = "";
if( parent->user.length() > 0 ) {
rtsp_username = escape_json_string(parent->user);
rtsp_password = escape_json_string(parent->pass);
}
if (Use_RTSP_Restream) {
int restream_port = config.min_rtsp_port;
rtsp_username = "";
rtsp_password = "";
rtsp_path = "rtsp://127.0.0.1:" + std::to_string(restream_port) + "/" + parent->rtsp_streamname;
} else {
std::size_t at_pos = parent->path.find("@", 7);
if (at_pos != std::string::npos) {
//If we find an @ symbol, we have a username/password. Otherwise, passwordless login.
std::size_t colon_pos = parent->path.find(":", 7); //Search for the colon, but only after the rtsp:// text.
if (colon_pos == std::string::npos) {
//Looks like an invalid url
throw std::runtime_error("Cannot Parse URL for Janus.");
}
rtsp_username = parent->path.substr(7, colon_pos-7);
rtsp_password = parent->path.substr(colon_pos+1, at_pos - colon_pos - 1);
rtsp_path = "rtsp://";
rtsp_path += parent->path.substr(at_pos + 1);
} else {
rtsp_username = "";
rtsp_password = "";
rtsp_path = parent->path;
}
rtsp_path = parent->path;
}
}
@@ -164,7 +156,7 @@ int Monitor::JanusManager::add_to_janus() {
postData += "\", \"videofmtp\" : \"";
postData += profile_override;
}
if (rtsp_username != "") {
if (rtsp_username.length() > 0) {
postData += "\", \"rtsp_user\" : \"";
postData += rtsp_username;
postData += "\", \"rtsp_pwd\" : \"";
@@ -292,3 +284,15 @@ int Monitor::JanusManager::get_janus_handle() {
janus_handle = response.substr(pos + 6, 16);
return 1;
} //get_janus_handle
std::string escape_json_string( std::string input ) {
std::string tmp;
tmp = regex_replace(input, std::regex("\n"), "\\n");
tmp = regex_replace(tmp, std::regex("\b"), "\\b");
tmp = regex_replace(tmp, std::regex("\f"), "\\f");
tmp = regex_replace(tmp, std::regex("\r"), "\\r");
tmp = regex_replace(tmp, std::regex("\t"), "\\t");
tmp = regex_replace(tmp, std::regex("\""), "\\\"");
tmp = regex_replace(tmp, std::regex("[\\\\]"), "\\\\");
return tmp;
}

View File

@@ -30,6 +30,8 @@ RemoteCameraRtsp::RemoteCameraRtsp(
const std::string &p_host,
const std::string &p_port,
const std::string &p_path,
const std::string &p_user,
const std::string &p_pass,
int p_width,
int p_height,
bool p_rtsp_describe,
@@ -47,6 +49,8 @@ RemoteCameraRtsp::RemoteCameraRtsp(
p_brightness, p_contrast, p_hue, p_colour,
p_capture, p_record_audio),
rtsp_describe(p_rtsp_describe),
user(p_user),
pass(p_pass),
frameCount(0)
{
if ( p_method == "rtpUni" )
@@ -110,7 +114,7 @@ void RemoteCameraRtsp::Terminate() {
}
int RemoteCameraRtsp::Connect() {
rtspThread = zm::make_unique<RtspThread>(monitor->Id(), method, protocol, host, port, path, auth, rtsp_describe);
rtspThread = zm::make_unique<RtspThread>(monitor->Id(), method, protocol, host, port, path, user, pass, rtsp_describe);
return 0;
}

View File

@@ -38,6 +38,9 @@ protected:
int rtcp_sd;
bool rtsp_describe;
const std::string user;
const std::string pass;
Buffer buffer;
Buffer lastSps;
Buffer lastPps;
@@ -57,6 +60,8 @@ public:
const std::string &host,
const std::string &port,
const std::string &path,
const std::string &user,
const std::string &pass,
int p_width,
int p_height,
bool p_rtsp_describe,

View File

@@ -135,7 +135,8 @@ RtspThread::RtspThread(
const std::string &host,
const std::string &port,
const std::string &path,
const std::string &auth,
const std::string &user,
const std::string &pass,
bool rtsp_describe) :
mId(id),
mMethod(method),
@@ -169,12 +170,16 @@ RtspThread::RtspThread(
mHttpSession = stringtf("%d", rand());
mNeedAuth = false;
StringVector parts = Split(auth, ":");
Debug(2, "# of auth parts %zu", parts.size());
if ( parts.size() > 1 )
mAuthenticator = new zm::Authenticator(parts[0], parts[1]);
else
mAuthenticator = new zm::Authenticator(parts[0], "");
if ( user.length() > 0 && pass.length() > 0 ) {
Debug(2, "# of auth parts 2");
mAuthenticator = new zm::Authenticator(user, pass);
} else if( user.length() > 0 ) {
Debug(2, "# of auth parts 1");
mAuthenticator = new zm::Authenticator(user, "");
} else {
Debug(2, "# of auth parts 0");
mAuthenticator = new zm::Authenticator("", "");
}
mThread = std::thread(&RtspThread::Run, this);
}

View File

@@ -96,7 +96,9 @@ private:
void Run();
public:
RtspThread( int id, RtspMethod method, const std::string &protocol, const std::string &host, const std::string &port, const std::string &path, const std::string &auth, bool rtsp_describe );
RtspThread( int id, RtspMethod method, const std::string &protocol, const std::string &host,
const std::string &port, const std::string &path, const std::string &user, const std::string &pass,
bool rtsp_describe );
~RtspThread();
public:

View File

@@ -379,6 +379,26 @@ std::string UriDecode(const std::string &encoded) {
return retbuf;
}
std::string UriEncode(const std::string &value) {
const char *src = value.c_str();
std::string retbuf;
retbuf.reserve(value.length() * 3); // at most all characters get replaced with the escape
char tmp[5] = "";
while(*src) {
if ( *src == ' ' ) {
retbuf.append("%%20");
} else if ( !( (*src >= 'a' && *src <= 'z') || (*src >= 'A' && *src <= 'Z') ) ) {
sprintf(tmp, "%%%02X", *src);
retbuf.append(tmp);
} else {
retbuf.push_back(*src);
}
src++;
}
return retbuf;
}
QueryString::QueryString(std::istream &input) {
while (!input.eof() && input.peek() > 0) {
//Should eat "param1="

View File

@@ -50,6 +50,10 @@ inline std::string StringToUpper(std::string str) {
std::transform(str.begin(), str.end(), str.begin(), ::toupper);
return str;
}
inline std::string StringToLower(std::string str) {
std::transform(str.begin(), str.end(), str.begin(), ::tolower);
return str;
}
StringVector Split(const std::string &str, char delim);
StringVector Split(const std::string &str, const std::string &delim, size_t limit = 0);
@@ -130,6 +134,7 @@ constexpr std::size_t size(const T(&)[N]) noexcept { return N; }
std::string mask_authentication(const std::string &url);
std::string remove_authentication(const std::string &url);
std::string UriEncode(const std::string &value);
std::string UriDecode(const std::string &encoded);
class QueryParameter {

View File

@@ -289,6 +289,78 @@ public static function getStatuses() {
return $this->{'Server'};
}
public function Path($new=null) {
// set the new value if requested
if( $new !== null ) {
$this->{'Path'} = $new;
}
$old_us = $this->{'User'};
$old_ps = $this->{'Pass'};
// empty value or old auth values terminate
if( strlen($this->{'Path'}) == 0 )
return $this->{'Path'};
// extract the authentication part from the path given
$values = extract_auth_values_from_url($this->{'Path'});
// If no values for User and Pass fields are present then terminate
if( count( $values ) !== 2 ) {
return $this->{'Path'};
}
$us = $values[0];
$ps = $values[1];
// Update the auth fields if they were empty and remove them from the path
// or if they are equal between the path and field
if( (strlen($old_us) == 0 || strlen($old_ps) == 0) ||
($us == $old_us && $ps == $old_ps) )
{
$this->{'Path'} = str_replace("$us:$ps@", "", $this->{'Path'});
$this->{'User'} = $us;
$this->{'Pass'} = $ps;
}
return $this->{'Path'};
}
public function User($new=null) {
if( $new !== null ) {
// no url check if the update has different value
$this->{'User'} = $new;
}
if( strlen($this->{'User'}) > 0 )
return $this->{'User'};
// Only try to update from path if the field is empty
$values = extract_auth_values_from_url($this->{'Path'});
if( count( $values ) == 2 ) {
$us = $values[0];
$this->{'User'} = $values[0];
}
return $this->{'User'};
}
public function Pass($new=null) {
if( $new !== null ) {
// no url check if the update has different value
$this->{'Pass'} = $new;
}
if( strlen($this->{'Pass'}) > 0 )
return $this->{'Pass'};
// Only try to update from path if the field is empty
$values = extract_auth_values_from_url($this->{'Path'});
if( count( $values ) == 2 ) {
$ps = $values[1];
$this->{'Pass'} = $values[1];
}
return $this->{'Pass'};
}
public function __call($fn, array $args) {
if (count($args)) {
if (is_array($this->defaults[$fn]) and $this->defaults[$fn]['type'] == 'set') {

View File

@@ -2345,4 +2345,23 @@ function get_subnets($interface) {
return $subnets;
}
function extract_auth_values_from_url($url): array {
$protocolPrefixPos = strpos($url, '://');
if( $protocolPrefixPos === false )
return array();
$authSeparatorPos = strpos($url, '@', $protocolPrefixPos+3);
if( $authSeparatorPos === false )
return array();
$fieldsSeparatorPos = strpos($url, ':', $protocolPrefixPos+3);
if( $fieldsSeparatorPos === false || $authSeparatorPos < $fieldsSeparatorPos )
return array();
$username = substr( $url, $protocolPrefixPos+3, $fieldsSeparatorPos-($protocolPrefixPos+3) );
$password = substr( $url, $fieldsSeparatorPos+1, $authSeparatorPos-$fieldsSeparatorPos-1 );
return array( $username, $password );
}
?>

View File

@@ -302,6 +302,18 @@ function initPage() {
}
});
let monitorPath = document.getElementsByName("newMonitor[Path]")[0];
monitorPath.addEventListener('keyup', change_Path); // on edit sync path -> user & pass
monitorPath.addEventListener('blur', change_Path); // remove fields from path if user & pass equal on end of edit
let monitorUser = document.getElementsByName("newMonitor[User]");
if( monitorUser.length > 0 )
monitorUser[0].addEventListener('blur', change_Path); // remove fields from path if user & pass equal
let monitorPass = document.getElementsByName("newMonitor[Pass]");
if( monitorPass.length > 0 )
monitorPass[0].addEventListener('blur', change_Path); // remove fields from path if user & pass equal
if ( parseInt(ZM_OPT_USE_GEOLOCATION) ) {
if ( window.L ) {
const form = document.getElementById('contentForm');
@@ -343,6 +355,64 @@ function initPage() {
updateLinkedMonitorsUI();
} // end function initPage()
function change_Path(event) {
var pathInput = document.getElementsByName("newMonitor[Path]")[0];
var protoPrefixPos = pathInput.value.indexOf('://');
if( protoPrefixPos == -1 )
return;
// check the formatting of the url
var authSeparatorPos = pathInput.value.indexOf( '@', protoPrefixPos+3 );
if( authSeparatorPos == -1 ) {
console.warn('ignoring URL incorrectly formatted, missing "@"');
return;
}
var fieldsSeparatorPos = pathInput.value.indexOf( ':', protoPrefixPos+3 );
if( authSeparatorPos == -1 || fieldsSeparatorPos >= authSeparatorPos ) {
console.warn('ignoring URL incorrectly formatted, missing ":"');
return;
}
var usernameValue = pathInput.value.substring( protoPrefixPos+3, fieldsSeparatorPos );
var passwordValue = pathInput.value.substring( fieldsSeparatorPos+1, authSeparatorPos );
if( usernameValue.length == 0 || passwordValue.length == 0 ) {
console.warn('ignoring URL incorrectly formatted, empty username or password');
return;
}
// get the username / password inputs
var userInput = document.getElementsByName("newMonitor[User]");
var passInput = document.getElementsByName("newMonitor[Pass]");
if (userInput.length != 1 || passInput.length != 1) {
return
}
// on editing update the fields only if they are empty or a prefix of the new value
if( event.type != 'blur' ) {
if( userInput[0].value.length == 0 || usernameValue.indexOf(userInput[0].value) == 0 ||
userInput[0].value.indexOf(usernameValue) == 0 )
{
userInput[0].value = usernameValue;
}
if( passInput[0].value.length == 0 || passwordValue.indexOf(passInput[0].value) == 0 ||
passInput[0].value.indexOf(passwordValue) == 0 )
{
passInput[0].value = passwordValue;
}
return;
}
// on leaving the input sync the values and remove it from the url
// only if they already match (to not overwrite already present values)
if( userInput[0].value == usernameValue && passInput[0].value == passwordValue )
pathInput.value = pathInput.value.substring(0, protoPrefixPos+3) + pathInput.value.substring(authSeparatorPos+1, pathInput.value.length);
}
function change_WebColour() {
$j('#WebSwatch').css(
'backgroundColor',

View File

@@ -688,6 +688,8 @@ include('_monitor_source_nvsocket.php');
<?php
} else if ( $monitor->Type() == 'Remote' ) {
?>
<tr><td class="text-right pr-3"><?php echo 'Username' ?></td><td><input type="text" name="newMonitor[User]" value="<?php echo validHtmlStr($monitor->User()) ?>"/></td></tr>
<tr><td class="text-right pr-3"><?php echo 'Password' ?></td><td><input type="text" name="newMonitor[Pass]" value="<?php echo validHtmlStr($monitor->Pass()) ?>"/></td></tr>
<tr>
<td class="text-right pr-3"><?php echo translate('RemoteProtocol') ?></td>
<td><?php echo htmlSelect('newMonitor[Protocol]', $remoteProtocols, $monitor->Protocol(), "updateMethods( this );if(this.value=='rtsp'){\$('RTSPDescribe').setStyle('display','table-row');}else{\$('RTSPDescribe').hide();}" ); ?></td>
@@ -748,6 +750,8 @@ include('_monitor_source_nvsocket.php');
<td class="text-right pr-3"><?php echo translate('SourcePath') ?></td>
<td><input type="text" name="newMonitor[Path]" value="<?php echo validHtmlStr($monitor->Path()) ?>" /></td>
</tr>
<tr><td class="text-right pr-3"><?php echo 'Username' ?></td><td><input type="text" name="newMonitor[User]" value="<?php echo validHtmlStr($monitor->User()) ?>"/></td></tr>
<tr><td class="text-right pr-3"><?php echo 'Password' ?></td><td><input type="text" name="newMonitor[Pass]" value="<?php echo validHtmlStr($monitor->Pass()) ?>"/></td></tr>
<tr>
<td class="text-right pr-3">
<?php echo translate('RemoteMethod'); echo makeHelpLink('OPTIONS_RTSPTrans') ?></td>