mirror of
https://github.com/Motion-Project/motion.git
synced 2025-12-23 23:18:21 -05:00
1198 lines
36 KiB
C
1198 lines
36 KiB
C
/*
|
|
* stream.c (based in webcam.c)
|
|
* Streaming using jpeg images over a multipart/x-mixed-replace stream
|
|
* Copyright (C) 2002 Jeroen Vreeken (pe1rxq@amsat.org)
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program 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 General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
|
|
*/
|
|
|
|
#include "md5.h"
|
|
#include "picture.h"
|
|
#include <sys/socket.h>
|
|
#include <netinet/in.h>
|
|
#include <arpa/inet.h>
|
|
#include <netdb.h>
|
|
#include <ctype.h>
|
|
#include <sys/fcntl.h>
|
|
|
|
#define STREAM_REALM "Motion Stream Security Access"
|
|
#define KEEP_ALIVE_TIMEOUT 100
|
|
|
|
typedef void* (*auth_handler)(void*);
|
|
struct auth_param {
|
|
struct context *cnt;
|
|
int sock;
|
|
int sock_flags;
|
|
int* thread_count;
|
|
struct config *conf;
|
|
};
|
|
|
|
pthread_mutex_t stream_auth_mutex;
|
|
|
|
/**
|
|
* set_sock_timeout
|
|
*
|
|
* Returns : 0 or 1 on timeout
|
|
*/
|
|
static int set_sock_timeout(int sock, int sec)
|
|
{
|
|
struct timeval tv;
|
|
|
|
tv.tv_sec = sec;
|
|
tv.tv_usec = 0;
|
|
|
|
if (setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (char*) &tv, sizeof(tv))) {
|
|
MOTION_LOG(ERR, TYPE_STREAM, SHOW_ERRNO, "%s: set socket timeout failed");
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* read_http_request
|
|
*
|
|
*
|
|
* Returns : 1 on success or 0 if any error happens
|
|
*/
|
|
static int read_http_request(int sock, char* buffer, int buflen, char* uri, int uri_len)
|
|
{
|
|
int nread = 0;
|
|
int ret,readb = 1;
|
|
char method[10] = {'\0'};
|
|
char url[512] = {'\0'};
|
|
char protocol[10] = {'\0'};
|
|
|
|
static const char *bad_request_response_raw =
|
|
"HTTP/1.0 400 Bad Request\r\n"
|
|
"Content-type: text/plain\r\n\r\n"
|
|
"Bad Request\n";
|
|
|
|
static const char *bad_method_response_template_raw =
|
|
"HTTP/1.0 501 Method Not Implemented\r\n"
|
|
"Content-type: text/plain\r\n\r\n"
|
|
"Method Not Implemented\n";
|
|
|
|
static const char *timeout_response_template_raw =
|
|
"HTTP/1.0 408 Request Timeout\r\n"
|
|
"Content-type: text/plain\r\n\r\n"
|
|
"Request Timeout\n";
|
|
|
|
buffer[0] = '\0';
|
|
|
|
while ((strstr(buffer, "\r\n\r\n") == NULL) && (readb != 0) && (nread < buflen)) {
|
|
|
|
readb = read(sock, buffer+nread, buflen - nread);
|
|
|
|
if (readb == -1) {
|
|
nread = -1;
|
|
break;
|
|
}
|
|
|
|
nread += readb;
|
|
|
|
if (nread > buflen) {
|
|
MOTION_LOG(ERR, TYPE_STREAM, SHOW_ERRNO, "%s: motion-stream End buffer reached"
|
|
" waiting for buffer ending");
|
|
break;
|
|
}
|
|
|
|
buffer[nread] = '\0';
|
|
}
|
|
|
|
/*
|
|
* Make sure the last read didn't fail. If it did, there's a
|
|
* problem with the connection, so give up.
|
|
*/
|
|
if (nread == -1) {
|
|
if(errno == EAGAIN) { // Timeout
|
|
ret = write(sock, timeout_response_template_raw, strlen(timeout_response_template_raw));
|
|
return 0;
|
|
}
|
|
|
|
MOTION_LOG(ERR, TYPE_STREAM, SHOW_ERRNO, "%s: motion-stream READ give up!");
|
|
return 0;
|
|
}
|
|
|
|
ret = sscanf(buffer, "%9s %511s %9s", method, url, protocol);
|
|
|
|
if (ret != 3) {
|
|
ret = write(sock, bad_request_response_raw, sizeof(bad_request_response_raw));
|
|
return 0;
|
|
}
|
|
|
|
/* Check Protocol */
|
|
if (strcmp(protocol, "HTTP/1.0") && strcmp (protocol, "HTTP/1.1")) {
|
|
/* We don't understand this protocol. Report a bad response. */
|
|
ret = write(sock, bad_request_response_raw, sizeof(bad_request_response_raw));
|
|
return 0;
|
|
}
|
|
|
|
if (strcmp(method, "GET")) {
|
|
/*
|
|
* This server only implements the GET method. If client
|
|
* uses other method, report the failure.
|
|
*/
|
|
char response[1024];
|
|
snprintf(response, sizeof(response), bad_method_response_template_raw, method);
|
|
ret = write(sock, response, strlen (response));
|
|
|
|
return 0;
|
|
}
|
|
|
|
if(uri)
|
|
strncpy(uri, url, uri_len);
|
|
|
|
return 1;
|
|
}
|
|
|
|
static void stream_add_client(struct stream *list, int sc);
|
|
|
|
/**
|
|
* handle_basic_auth
|
|
*
|
|
*
|
|
*/
|
|
static void* handle_basic_auth(void* param)
|
|
{
|
|
struct auth_param *p = (struct auth_param*)param;
|
|
char buffer[1024] = {'\0'};
|
|
ssize_t length = 1023;
|
|
char *auth, *h, *authentication;
|
|
static const char *request_auth_response_template=
|
|
"HTTP/1.0 401 Authorization Required\r\n"
|
|
"Server: Motion/"VERSION"\r\n"
|
|
"Max-Age: 0\r\n"
|
|
"Expires: 0\r\n"
|
|
"Cache-Control: no-cache, private\r\n"
|
|
"Pragma: no-cache\r\n"
|
|
"WWW-Authenticate: Basic realm=\""STREAM_REALM"\"\r\n\r\n";
|
|
|
|
#ifdef HAVE_PTHREAD_SETNAME_NP
|
|
pthread_setname_np(pthread_self(), "handle_basic_auth");
|
|
#endif
|
|
|
|
pthread_mutex_lock(&stream_auth_mutex);
|
|
p->thread_count++;
|
|
pthread_mutex_unlock(&stream_auth_mutex);
|
|
|
|
if (!read_http_request(p->sock,buffer, length, NULL, 0))
|
|
goto Invalid_Request;
|
|
|
|
|
|
auth = strstr(buffer, "Authorization: Basic");
|
|
|
|
if (!auth)
|
|
goto Error;
|
|
|
|
auth += sizeof("Authorization: Basic");
|
|
h = strstr(auth, "\r\n");
|
|
|
|
if(!h)
|
|
goto Error;
|
|
|
|
*h='\0';
|
|
|
|
if (p->conf->stream_authentication != NULL) {
|
|
|
|
char *userpass = NULL;
|
|
size_t auth_size = strlen(p->conf->stream_authentication);
|
|
|
|
authentication = mymalloc(BASE64_LENGTH(auth_size) + 1);
|
|
userpass = mymalloc(auth_size + 4);
|
|
/* base64_encode can read 3 bytes after the end of the string, initialize it. */
|
|
memset(userpass, 0, auth_size + 4);
|
|
strcpy(userpass, p->conf->stream_authentication);
|
|
base64_encode(userpass, authentication, auth_size);
|
|
free(userpass);
|
|
|
|
if (strcmp(auth, authentication)) {
|
|
free(authentication);
|
|
goto Error;
|
|
}
|
|
free(authentication);
|
|
}
|
|
|
|
// OK - Access
|
|
|
|
/* Set socket to non blocking */
|
|
if (fcntl(p->sock, F_SETFL, p->sock_flags) < 0) {
|
|
MOTION_LOG(ERR, TYPE_STREAM, SHOW_ERRNO, "%s: fcntl");
|
|
goto Error;
|
|
}
|
|
|
|
/* Lock the mutex */
|
|
pthread_mutex_lock(&stream_auth_mutex);
|
|
|
|
stream_add_client(&p->cnt->stream, p->sock);
|
|
p->cnt->stream_count++;
|
|
p->thread_count--;
|
|
|
|
/* Unlock the mutex */
|
|
pthread_mutex_unlock(&stream_auth_mutex);
|
|
|
|
free(p);
|
|
pthread_exit(NULL);
|
|
|
|
Error:
|
|
if (write(p->sock, request_auth_response_template, strlen (request_auth_response_template)) < 0)
|
|
MOTION_LOG(DBG, TYPE_STREAM, SHOW_ERRNO, "%s: write failure 1:handle_basic_auth");
|
|
|
|
Invalid_Request:
|
|
close(p->sock);
|
|
|
|
pthread_mutex_lock(&stream_auth_mutex);
|
|
p->thread_count--;
|
|
pthread_mutex_unlock(&stream_auth_mutex);
|
|
|
|
free(p);
|
|
pthread_exit(NULL);
|
|
}
|
|
|
|
|
|
#define HASHLEN 16
|
|
typedef char HASH[HASHLEN];
|
|
#define HASHHEXLEN 32
|
|
typedef char HASHHEX[HASHHEXLEN+1];
|
|
#define IN
|
|
#define OUT
|
|
/**
|
|
* CvtHex
|
|
* Calculates H(A1) as per HTTP Digest spec -- taken from RFC 2617.
|
|
*/
|
|
static void CvtHex(IN HASH Bin, OUT HASHHEX Hex)
|
|
{
|
|
unsigned short i;
|
|
unsigned char j;
|
|
|
|
for (i = 0; i < HASHLEN; i++) {
|
|
j = (Bin[i] >> 4) & 0xf;
|
|
if (j <= 9)
|
|
Hex[i*2] = (j + '0');
|
|
else
|
|
Hex[i*2] = (j + 'a' - 10);
|
|
j = Bin[i] & 0xf;
|
|
if (j <= 9)
|
|
Hex[i*2+1] = (j + '0');
|
|
else
|
|
Hex[i*2+1] = (j + 'a' - 10);
|
|
};
|
|
Hex[HASHHEXLEN] = '\0';
|
|
};
|
|
|
|
/**
|
|
* DigestCalcHA1
|
|
* Calculates H(A1) as per spec.
|
|
*/
|
|
static void DigestCalcHA1(
|
|
IN char * pszAlg,
|
|
IN char * pszUserName,
|
|
IN char * pszRealm,
|
|
IN char * pszPassword,
|
|
IN char * pszNonce,
|
|
IN char * pszCNonce,
|
|
OUT HASHHEX SessionKey
|
|
)
|
|
{
|
|
MD5_CTX Md5Ctx;
|
|
HASH HA1;
|
|
|
|
MD5Init(&Md5Ctx);
|
|
MD5Update(&Md5Ctx, (unsigned char *)pszUserName, strlen(pszUserName));
|
|
MD5Update(&Md5Ctx, (unsigned char *)":", 1);
|
|
MD5Update(&Md5Ctx, (unsigned char *)pszRealm, strlen(pszRealm));
|
|
MD5Update(&Md5Ctx, (unsigned char *)":", 1);
|
|
MD5Update(&Md5Ctx, (unsigned char *)pszPassword, strlen(pszPassword));
|
|
MD5Final((unsigned char *)HA1, &Md5Ctx);
|
|
|
|
if (strcmp(pszAlg, "md5-sess") == 0) {
|
|
MD5Init(&Md5Ctx);
|
|
MD5Update(&Md5Ctx, (unsigned char *)HA1, HASHLEN);
|
|
MD5Update(&Md5Ctx, (unsigned char *)":", 1);
|
|
MD5Update(&Md5Ctx, (unsigned char *)pszNonce, strlen(pszNonce));
|
|
MD5Update(&Md5Ctx, (unsigned char *)":", 1);
|
|
MD5Update(&Md5Ctx, (unsigned char *)pszCNonce, strlen(pszCNonce));
|
|
MD5Final((unsigned char *)HA1, &Md5Ctx);
|
|
};
|
|
CvtHex(HA1, SessionKey);
|
|
};
|
|
|
|
/**
|
|
* DigestCalcResponse
|
|
* Calculates request-digest/response-digest as per HTTP Digest spec.
|
|
*/
|
|
static void DigestCalcResponse(
|
|
IN HASHHEX HA1, /* H(A1) */
|
|
IN char * pszNonce, /* nonce from server */
|
|
IN char * pszNonceCount, /* 8 hex digits */
|
|
IN char * pszCNonce, /* client nonce */
|
|
IN char * pszQop, /* qop-value: "", "auth", "auth-int" */
|
|
IN char * pszMethod, /* method from the request */
|
|
IN char * pszDigestUri, /* requested URL */
|
|
IN HASHHEX HEntity, /* H(entity body) if qop="auth-int" */
|
|
OUT HASHHEX Response /* request-digest or response-digest */
|
|
)
|
|
{
|
|
MD5_CTX Md5Ctx;
|
|
HASH HA2;
|
|
HASH RespHash;
|
|
HASHHEX HA2Hex;
|
|
|
|
// Calculate H(A2)
|
|
MD5Init(&Md5Ctx);
|
|
MD5Update(&Md5Ctx, (unsigned char *)pszMethod, strlen(pszMethod));
|
|
MD5Update(&Md5Ctx, (unsigned char *)":", 1);
|
|
MD5Update(&Md5Ctx, (unsigned char *)pszDigestUri, strlen(pszDigestUri));
|
|
|
|
if (strcmp(pszQop, "auth-int") == 0) {
|
|
MD5Update(&Md5Ctx, (unsigned char *)":", 1);
|
|
MD5Update(&Md5Ctx, (unsigned char *)HEntity, HASHHEXLEN);
|
|
}
|
|
MD5Final((unsigned char *)HA2, &Md5Ctx);
|
|
CvtHex(HA2, HA2Hex);
|
|
|
|
// Calculate response
|
|
MD5Init(&Md5Ctx);
|
|
MD5Update(&Md5Ctx, (unsigned char *)HA1, HASHHEXLEN);
|
|
MD5Update(&Md5Ctx, (unsigned char *)":", 1);
|
|
MD5Update(&Md5Ctx, (unsigned char *)pszNonce, strlen(pszNonce));
|
|
MD5Update(&Md5Ctx, (unsigned char *)":", 1);
|
|
|
|
if (*pszQop) {
|
|
MD5Update(&Md5Ctx, (unsigned char *)pszNonceCount, strlen(pszNonceCount));
|
|
MD5Update(&Md5Ctx, (unsigned char *)":", 1);
|
|
MD5Update(&Md5Ctx, (unsigned char *)pszCNonce, strlen(pszCNonce));
|
|
MD5Update(&Md5Ctx, (unsigned char *)":", 1);
|
|
MD5Update(&Md5Ctx, (unsigned char *)pszQop, strlen(pszQop));
|
|
MD5Update(&Md5Ctx, (unsigned char *)":", 1);
|
|
}
|
|
MD5Update(&Md5Ctx, (unsigned char *)HA2Hex, HASHHEXLEN);
|
|
MD5Final((unsigned char *)RespHash, &Md5Ctx);
|
|
CvtHex(RespHash, Response);
|
|
};
|
|
|
|
|
|
/**
|
|
* handle_md5_digest
|
|
*
|
|
*
|
|
*/
|
|
static void* handle_md5_digest(void* param)
|
|
{
|
|
struct auth_param *p = (struct auth_param*)param;
|
|
char buffer[1024] = {'\0'};
|
|
ssize_t length = 1023;
|
|
char *auth, *h, *username, *realm, *uri, *nonce, *response;
|
|
int username_len, realm_len, uri_len, nonce_len, response_len;
|
|
#define SERVER_NONCE_LEN 17
|
|
char server_nonce[SERVER_NONCE_LEN];
|
|
#define SERVER_URI_LEN 512
|
|
char server_uri[SERVER_URI_LEN];
|
|
char* server_user = NULL, *server_pass = NULL;
|
|
unsigned int rand1,rand2;
|
|
HASHHEX HA1;
|
|
HASHHEX HA2 = "";
|
|
HASHHEX server_response;
|
|
static const char *request_auth_response_template=
|
|
"HTTP/1.0 401 Authorization Required\r\n"
|
|
"Server: Motion/"VERSION"\r\n"
|
|
"Max-Age: 0\r\n"
|
|
"Expires: 0\r\n"
|
|
"Cache-Control: no-cache, private\r\n"
|
|
"Pragma: no-cache\r\n"
|
|
"WWW-Authenticate: Digest";
|
|
static const char *auth_failed_html_template=
|
|
"<!DOCTYPE html>\n"
|
|
"<html>\n"
|
|
"<head><title>401 Authorization Required</title></head>\n"
|
|
"<body>\n"
|
|
"<h1>Authorization Required</h1>\n"
|
|
"<p>This server could not verify that you are authorized to access the document "
|
|
"requested. Either you supplied the wrong credentials (e.g., bad password), "
|
|
"or your browser doesn't understand how to supply the credentials required.</p>\n"
|
|
"</body>\n"
|
|
"</html>\n";
|
|
static const char *internal_error_template=
|
|
"HTTP/1.0 500 Internal Server Error\r\n"
|
|
"Server: Motion/"VERSION"\r\n"
|
|
"Content-Type: text/html\r\n"
|
|
"Connection: Close\r\n\r\n"
|
|
"<!DOCTYPE html>\n"
|
|
"<html>\n"
|
|
"<head><title>500 Internal Server Error</title></head>\n"
|
|
"<body>\n"
|
|
"<h1>500 Internal Server Error</h1>\n"
|
|
"</body>\n"
|
|
"</html>\n";
|
|
|
|
#ifdef HAVE_PTHREAD_SETNAME_NP
|
|
pthread_setname_np(pthread_self(), "handle_md5_digest");
|
|
#endif
|
|
|
|
pthread_mutex_lock(&stream_auth_mutex);
|
|
p->thread_count++;
|
|
pthread_mutex_unlock(&stream_auth_mutex);
|
|
|
|
set_sock_timeout(p->sock, KEEP_ALIVE_TIMEOUT);
|
|
srand(time(NULL));
|
|
rand1 = (unsigned int)(42000000.0 * rand() / (RAND_MAX + 1.0));
|
|
rand2 = (unsigned int)(42000000.0 * rand() / (RAND_MAX + 1.0));
|
|
snprintf(server_nonce, SERVER_NONCE_LEN, "%08x%08x", rand1, rand2);
|
|
|
|
if (!p->conf->stream_authentication) {
|
|
MOTION_LOG(ERR, TYPE_STREAM, SHOW_ERRNO, "%s: Error no authentication data");
|
|
goto InternalError;
|
|
}
|
|
h = strstr(p->conf->stream_authentication, ":");
|
|
|
|
if (!h) {
|
|
MOTION_LOG(ERR, TYPE_STREAM, SHOW_ERRNO, "%s: Error no authentication data (no ':' found)");
|
|
goto InternalError;
|
|
}
|
|
|
|
server_user = (char*)malloc((h - p->conf->stream_authentication) + 1);
|
|
server_pass = (char*)malloc(strlen(h) + 1);
|
|
|
|
if (!server_user || !server_pass) {
|
|
MOTION_LOG(ERR, TYPE_STREAM, SHOW_ERRNO, "%s: Error malloc failed");
|
|
goto InternalError;
|
|
}
|
|
|
|
strncpy(server_user, p->conf->stream_authentication, h-p->conf->stream_authentication);
|
|
server_user[h - p->conf->stream_authentication] = '\0';
|
|
strncpy(server_pass, h + 1, strlen(h + 1));
|
|
server_pass[strlen(h + 1)] = '\0';
|
|
|
|
while(1) {
|
|
if(!read_http_request(p->sock, buffer, length, server_uri, SERVER_URI_LEN - 1))
|
|
goto Invalid_Request;
|
|
|
|
auth = strstr(buffer, "Authorization: Digest");
|
|
if(!auth)
|
|
goto Error;
|
|
|
|
auth += sizeof("Authorization: Digest");
|
|
h = strstr(auth, "\r\n");
|
|
|
|
if (!h)
|
|
goto Error;
|
|
*h = '\0';
|
|
|
|
// Username
|
|
h=strstr(auth, "username=\"");
|
|
|
|
if (!h)
|
|
goto Error;
|
|
|
|
username = h + 10;
|
|
h = strstr(username + 1, "\"");
|
|
|
|
if (!h)
|
|
goto Error;
|
|
|
|
username_len = h - username;
|
|
|
|
// Realm
|
|
h = strstr(auth, "realm=\"");
|
|
if (!h)
|
|
goto Error;
|
|
|
|
realm = h + 7;
|
|
h = strstr(realm + 1, "\"");
|
|
|
|
if (!h)
|
|
goto Error;
|
|
|
|
realm_len = h - realm;
|
|
|
|
// URI
|
|
h = strstr(auth, "uri=\"");
|
|
|
|
if (!h)
|
|
goto Error;
|
|
|
|
uri = h + 5;
|
|
h = strstr(uri + 1, "\"");
|
|
|
|
if (!h)
|
|
goto Error;
|
|
|
|
uri_len = h - uri;
|
|
|
|
// Nonce
|
|
h = strstr(auth, "nonce=\"");
|
|
|
|
if (!h)
|
|
goto Error;
|
|
|
|
nonce = h + 7;
|
|
h = strstr(nonce + 1, "\"");
|
|
|
|
if (!h)
|
|
goto Error;
|
|
|
|
nonce_len = h - nonce;
|
|
|
|
// Response
|
|
h = strstr(auth, "response=\"");
|
|
|
|
if (!h)
|
|
goto Error;
|
|
|
|
response = h + 10;
|
|
h = strstr(response + 1, "\"");
|
|
|
|
if (!h)
|
|
goto Error;
|
|
|
|
response_len = h - response;
|
|
|
|
username[username_len] = '\0';
|
|
realm[realm_len] = '\0';
|
|
uri[uri_len] = '\0';
|
|
nonce[nonce_len] = '\0';
|
|
response[response_len] = '\0';
|
|
|
|
DigestCalcHA1((char*)"md5", server_user, (char*)STREAM_REALM, server_pass, (char*)server_nonce, (char*)NULL, HA1);
|
|
DigestCalcResponse(HA1, server_nonce, NULL, NULL, (char*)"", (char*)"GET", server_uri, HA2, server_response);
|
|
|
|
if (strcmp(server_response, response) == 0)
|
|
break;
|
|
Error:
|
|
rand1 = (unsigned int)(42000000.0 * rand() / (RAND_MAX + 1.0));
|
|
rand2 = (unsigned int)(42000000.0 * rand() / (RAND_MAX + 1.0));
|
|
snprintf(server_nonce, SERVER_NONCE_LEN, "%08x%08x", rand1, rand2);
|
|
snprintf(buffer, length, "%s realm=\""STREAM_REALM"\", nonce=\"%s\"\r\n"
|
|
"Content-Type: text/html\r\n"
|
|
"Keep-Alive: timeout=%i\r\n"
|
|
"Connection: keep-alive\r\n"
|
|
"Content-Length: %Zu\r\n\r\n",
|
|
request_auth_response_template, server_nonce,
|
|
KEEP_ALIVE_TIMEOUT, strlen(auth_failed_html_template));
|
|
if (write(p->sock, buffer, strlen(buffer)) < 0)
|
|
MOTION_LOG(DBG, TYPE_STREAM, SHOW_ERRNO, "%s: write failure 1:handle_md5_digest");
|
|
if (write(p->sock, auth_failed_html_template, strlen(auth_failed_html_template)) < 0)
|
|
MOTION_LOG(DBG, TYPE_STREAM, SHOW_ERRNO, "%s: write failure 2:handle_md5_digest");
|
|
}
|
|
|
|
// OK - Access
|
|
|
|
/* Set socket to non blocking */
|
|
if (fcntl(p->sock, F_SETFL, p->sock_flags) < 0) {
|
|
MOTION_LOG(ERR, TYPE_STREAM, SHOW_ERRNO, "%s: fcntl");
|
|
goto Error;
|
|
}
|
|
|
|
free(server_user);
|
|
free(server_pass);
|
|
|
|
/* Lock the mutex */
|
|
pthread_mutex_lock(&stream_auth_mutex);
|
|
|
|
stream_add_client(&p->cnt->stream, p->sock);
|
|
p->cnt->stream_count++;
|
|
|
|
p->thread_count--;
|
|
/* Unlock the mutex */
|
|
pthread_mutex_unlock(&stream_auth_mutex);
|
|
|
|
free(p);
|
|
pthread_exit(NULL);
|
|
|
|
InternalError:
|
|
free(server_user);
|
|
free(server_pass);
|
|
|
|
if (write(p->sock, internal_error_template, strlen(internal_error_template)) < 0)
|
|
MOTION_LOG(DBG, TYPE_STREAM, SHOW_ERRNO, "%s: write failure 3:handle_md5_digest");
|
|
|
|
Invalid_Request:
|
|
close(p->sock);
|
|
|
|
pthread_mutex_lock(&stream_auth_mutex);
|
|
p->thread_count--;
|
|
pthread_mutex_unlock(&stream_auth_mutex);
|
|
|
|
free(p);
|
|
pthread_exit(NULL);
|
|
}
|
|
|
|
/**
|
|
* do_client_auth
|
|
*
|
|
*
|
|
*/
|
|
static void do_client_auth(struct context *cnt, int sc)
|
|
{
|
|
pthread_t thread_id;
|
|
pthread_attr_t attr;
|
|
auth_handler handle_func;
|
|
struct auth_param* handle_param = NULL;
|
|
int flags;
|
|
static int first_call = 0;
|
|
static int thread_count = 0;
|
|
|
|
if(first_call == 0) {
|
|
first_call = 1;
|
|
/* Initialize the mutex */
|
|
pthread_mutex_init(&stream_auth_mutex, NULL);
|
|
}
|
|
|
|
switch(cnt->conf.stream_auth_method)
|
|
{
|
|
case 1: // Basic
|
|
handle_func = handle_basic_auth;
|
|
break;
|
|
case 2: // MD5 Digest
|
|
handle_func = handle_md5_digest;
|
|
break;
|
|
default:
|
|
MOTION_LOG(ERR, TYPE_STREAM, SHOW_ERRNO, "%s: Error unknown stream authentication method");
|
|
goto Error;
|
|
break;
|
|
}
|
|
|
|
handle_param = mymalloc(sizeof(struct auth_param));
|
|
handle_param->cnt = cnt;
|
|
handle_param->sock = sc;
|
|
handle_param->conf = &cnt->conf;
|
|
handle_param->thread_count = &thread_count;
|
|
|
|
/* Set socket to blocking */
|
|
if ((flags = fcntl(sc, F_GETFL, 0)) < 0) {
|
|
MOTION_LOG(ERR, TYPE_STREAM, SHOW_ERRNO, "%s: fcntl");
|
|
goto Error;
|
|
}
|
|
handle_param->sock_flags = flags;
|
|
|
|
if (fcntl(sc, F_SETFL, flags & (~O_NONBLOCK)) < 0) {
|
|
MOTION_LOG(ERR, TYPE_STREAM, SHOW_ERRNO, "%s: fcntl");
|
|
goto Error;
|
|
}
|
|
|
|
if (thread_count >= DEF_MAXSTREAMS)
|
|
goto Error;
|
|
|
|
if (pthread_attr_init(&attr)) {
|
|
MOTION_LOG(ERR, TYPE_STREAM, SHOW_ERRNO, "%s: Error pthread_attr_init");
|
|
goto Error;
|
|
}
|
|
|
|
if (pthread_create(&thread_id, &attr, handle_func, handle_param)) {
|
|
MOTION_LOG(ERR, TYPE_STREAM, SHOW_ERRNO, "%s: Error pthread_create");
|
|
goto Error;
|
|
}
|
|
pthread_detach(thread_id);
|
|
|
|
if (pthread_attr_destroy(&attr))
|
|
MOTION_LOG(ERR, TYPE_STREAM, SHOW_ERRNO, "%s: Error pthread_attr_destroy");
|
|
|
|
return;
|
|
|
|
Error:
|
|
close(sc);
|
|
free(handle_param);
|
|
}
|
|
|
|
/**
|
|
* http_bindsock
|
|
* Sets up a TCP/IP socket for incoming requests. It is called only during
|
|
* initialisation of Motion from the function stream_init
|
|
* The function sets up a a socket on the port number given by _port_.
|
|
* If the parameter _local_ is not zero the socket is setup to only accept connects from localhost.
|
|
* Otherwise any client IP address is accepted. The function returns an integer representing the socket.
|
|
*
|
|
* Returns: socket descriptor or -1 if any error happens
|
|
*/
|
|
int http_bindsock(int port, int local, int ipv6_enabled)
|
|
{
|
|
int sd = socket(ipv6_enabled?AF_INET6:AF_INET, SOCK_STREAM, 0);
|
|
|
|
int yes = 1, no = 0;
|
|
setsockopt(sd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
|
|
if (ipv6_enabled)
|
|
setsockopt(sd, IPPROTO_IPV6, IPV6_V6ONLY, &no, sizeof(no));
|
|
|
|
const char *addr_str;
|
|
struct sockaddr_storage sin;
|
|
bzero(&sin, sizeof(struct sockaddr_storage));
|
|
sin.ss_family = ipv6_enabled?AF_INET6:AF_INET;
|
|
if (ipv6_enabled) {
|
|
struct sockaddr_in6 *sin6 = (struct sockaddr_in6*)&sin;
|
|
sin6->sin6_family = AF_INET6;
|
|
sin6->sin6_port = htons(port);
|
|
if(local) {
|
|
addr_str = "::1";
|
|
sin6->sin6_addr = in6addr_loopback;
|
|
} else {
|
|
addr_str = "any IPv4/IPv6 address";
|
|
sin6->sin6_addr = in6addr_any;
|
|
}
|
|
} else {
|
|
struct sockaddr_in *sin4 = (struct sockaddr_in*)&sin;
|
|
sin4->sin_family = AF_INET;
|
|
sin4->sin_port = htons(port);
|
|
if(local) {
|
|
addr_str = "127.0.0.1";
|
|
sin4->sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
} else {
|
|
addr_str = "any IPv4 address";
|
|
sin4->sin_addr.s_addr = htonl(INADDR_ANY);
|
|
}
|
|
}
|
|
|
|
if (bind(sd, (struct sockaddr*)&sin, sizeof(sin)) != 0) {
|
|
MOTION_LOG(NTC, TYPE_STREAM, NO_ERRNO, "%s: error binding on %s port %d", addr_str, port);
|
|
close(sd);
|
|
return -1;
|
|
}
|
|
|
|
if (listen(sd, DEF_MAXWEBQUEUE) != 0) {
|
|
MOTION_LOG(CRT, TYPE_STREAM, SHOW_ERRNO, "%s: error listening");
|
|
close(sd);
|
|
return -1;
|
|
}
|
|
|
|
MOTION_LOG(NTC, TYPE_STREAM, NO_ERRNO, "%s: listening on %s port %d", addr_str, port);
|
|
|
|
return sd;
|
|
}
|
|
|
|
/**
|
|
* http_acceptsock
|
|
*
|
|
*
|
|
* Returns: socket descriptor or -1 if any error happens.
|
|
*/
|
|
static int http_acceptsock(int sl)
|
|
{
|
|
int sc;
|
|
struct sockaddr_storage addr;
|
|
socklen_t addr_len = sizeof(addr);
|
|
sc = accept(sl, (struct sockaddr*)&addr, &addr_len);
|
|
|
|
if (sc < 0) {
|
|
MOTION_LOG(CRT, TYPE_STREAM, SHOW_ERRNO, "%s: motion-stream accept()");
|
|
return -1;
|
|
}
|
|
|
|
unsigned long i = 1;
|
|
ioctl(sc, FIONBIO, &i);
|
|
return sc;
|
|
}
|
|
|
|
|
|
/**
|
|
* stream_flush
|
|
* Sends any outstanding data to all connected clients.
|
|
* It continuously goes through the client list until no data is able
|
|
* to be sent (either because there isn't any, or because the clients
|
|
* are not able to accept it).
|
|
*/
|
|
static void stream_flush(struct stream *list, int *stream_count, int lim)
|
|
{
|
|
int written; /* The number of bytes actually written. */
|
|
struct stream *client; /* Pointer to the client being served. */
|
|
int workdone = 0; /* Flag set any time data is successfully
|
|
written. */
|
|
|
|
client = list->next;
|
|
|
|
while (client) {
|
|
|
|
/* If data waiting for client, try to send it. */
|
|
if (client->tmpbuffer) {
|
|
|
|
/*
|
|
* We expect that list->filepos < list->tmpbuffer->size
|
|
* should always be true. The check is more for safety,
|
|
* in case of trouble is some other part of the code.
|
|
* Note that if it is false, the following section will
|
|
* clean up.
|
|
*/
|
|
if (client->filepos < client->tmpbuffer->size) {
|
|
|
|
/*
|
|
* Here we are finally ready to write out the
|
|
* data. Remember that (because the socket
|
|
* has been set non-blocking) we may only
|
|
* write out part of the buffer. The var
|
|
* 'filepos' contains how much of the buffer
|
|
* has already been written.
|
|
*/
|
|
written = write(client->socket,
|
|
client->tmpbuffer->ptr + client->filepos,
|
|
client->tmpbuffer->size - client->filepos);
|
|
|
|
/*
|
|
* If any data has been written, update the
|
|
* data pointer and set the workdone flag.
|
|
*/
|
|
if (written > 0) {
|
|
client->filepos += written;
|
|
workdone = 1;
|
|
}
|
|
} else
|
|
written = 0;
|
|
|
|
/*
|
|
* If we have written the entire buffer to the socket,
|
|
* or if there was some error (other than EAGAIN, which
|
|
* means the system couldn't take it), this request is
|
|
* finished.
|
|
*/
|
|
if ((client->filepos >= client->tmpbuffer->size) ||
|
|
(written < 0 && errno != EAGAIN)) {
|
|
/* If no other clients need this buffer, free it. */
|
|
if (--client->tmpbuffer->ref <= 0) {
|
|
free(client->tmpbuffer->ptr);
|
|
free(client->tmpbuffer);
|
|
}
|
|
|
|
/* Mark this client's buffer as empty. */
|
|
client->tmpbuffer = NULL;
|
|
client->nr++;
|
|
}
|
|
|
|
/*
|
|
* If the client is no longer connected, or the total
|
|
* number of frames already sent to this client is
|
|
* greater than our configuration limit, disconnect
|
|
* the client and free the stream struct.
|
|
*/
|
|
if ((written < 0 && errno != EAGAIN) ||
|
|
(lim && !client->tmpbuffer && client->nr > lim)) {
|
|
void *tmp;
|
|
|
|
close(client->socket);
|
|
|
|
if (client->next)
|
|
client->next->prev = client->prev;
|
|
|
|
client->prev->next = client->next;
|
|
tmp = client;
|
|
client = client->prev;
|
|
free(tmp);
|
|
(*stream_count)--;
|
|
}
|
|
} /* End if (client->tmpbuffer) */
|
|
|
|
/*
|
|
* Step the the next client in the list. If we get to the
|
|
* end of the list, check if anything was written during
|
|
* that loop; (if so) reset the 'workdone' flag and go back
|
|
* to the beginning.
|
|
*/
|
|
client = client->next;
|
|
|
|
if (!client && workdone) {
|
|
client = list->next;
|
|
workdone = 0;
|
|
}
|
|
} /* End while (client) */
|
|
}
|
|
|
|
/**
|
|
* stream_tmpbuffer
|
|
* Routine to create a new "tmpbuffer", which is a common
|
|
* object used by all clients connected to a single camera.
|
|
*
|
|
* Returns: new allocated stream_buffer.
|
|
*/
|
|
static struct stream_buffer *stream_tmpbuffer(int size)
|
|
{
|
|
struct stream_buffer *tmpbuffer = mymalloc(sizeof(struct stream_buffer));
|
|
tmpbuffer->ref = 0;
|
|
tmpbuffer->ptr = mymalloc(size);
|
|
|
|
return tmpbuffer;
|
|
}
|
|
|
|
/**
|
|
* stream_add_client
|
|
*
|
|
*
|
|
*/
|
|
static void stream_add_client(struct stream *list, int sc)
|
|
{
|
|
struct stream *new = mymalloc(sizeof(struct stream));
|
|
static const char header[] = "HTTP/1.0 200 OK\r\n"
|
|
"Server: Motion/"VERSION"\r\n"
|
|
"Connection: close\r\n"
|
|
"Max-Age: 0\r\n"
|
|
"Expires: 0\r\n"
|
|
"Cache-Control: no-cache, private\r\n"
|
|
"Pragma: no-cache\r\n"
|
|
"Content-Type: multipart/x-mixed-replace; "
|
|
"boundary=BoundaryString\r\n\r\n";
|
|
|
|
memset(new, 0, sizeof(struct stream));
|
|
new->socket = sc;
|
|
|
|
if ((new->tmpbuffer = stream_tmpbuffer(sizeof(header))) == NULL) {
|
|
MOTION_LOG(ERR, TYPE_STREAM, SHOW_ERRNO, "%s: Error creating tmpbuffer in stream_add_client");
|
|
} else {
|
|
memcpy(new->tmpbuffer->ptr, header, sizeof(header)-1);
|
|
new->tmpbuffer->size = sizeof(header)-1;
|
|
}
|
|
|
|
new->prev = list;
|
|
new->next = list->next;
|
|
|
|
if (new->next)
|
|
new->next->prev = new;
|
|
|
|
list->next = new;
|
|
}
|
|
|
|
/**
|
|
* stream_add_write
|
|
*
|
|
*
|
|
*/
|
|
static void stream_add_write(struct stream *list, struct stream_buffer *tmpbuffer, unsigned int fps)
|
|
{
|
|
struct timeval curtimeval;
|
|
unsigned long int curtime;
|
|
|
|
gettimeofday(&curtimeval, NULL);
|
|
curtime = curtimeval.tv_usec + 1000000L * curtimeval.tv_sec;
|
|
|
|
while (list->next) {
|
|
list = list->next;
|
|
|
|
if (list->tmpbuffer == NULL && ((curtime - list->last) >= 1000000L / fps)) {
|
|
list->last = curtime;
|
|
list->tmpbuffer = tmpbuffer;
|
|
tmpbuffer->ref++;
|
|
list->filepos = 0;
|
|
}
|
|
}
|
|
|
|
if (tmpbuffer->ref <= 0) {
|
|
free(tmpbuffer->ptr);
|
|
free(tmpbuffer);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* stream_check_write
|
|
* We walk through the chain of stream structs until we reach the end.
|
|
* Here we check if the tmpbuffer points to NULL.
|
|
* We return 1 if it finds a list->tmpbuffer which is a NULL pointer which would
|
|
* be the next client ready to be sent a new image. If not a 0 is returned.
|
|
*
|
|
* Returns:
|
|
*/
|
|
static int stream_check_write(struct stream *list)
|
|
{
|
|
while (list->next) {
|
|
list = list->next;
|
|
|
|
if (list->tmpbuffer == NULL)
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
/**
|
|
* stream_init
|
|
* This function is called from motion.c for each motion thread starting up.
|
|
* The function setup the incoming tcp socket that the clients connect to.
|
|
* The function returns an integer representing the socket.
|
|
*
|
|
* Returns: stream socket descriptor.
|
|
*/
|
|
int stream_init(struct context *cnt)
|
|
{
|
|
cnt->stream.socket = http_bindsock(cnt->conf.stream_port, cnt->conf.stream_localhost,
|
|
cnt->conf.ipv6_enabled);
|
|
cnt->stream.next = NULL;
|
|
cnt->stream.prev = NULL;
|
|
return cnt->stream.socket;
|
|
}
|
|
|
|
/**
|
|
* stream_stop
|
|
* This function is called from the motion_loop when it ends
|
|
* and motion is terminated or restarted.
|
|
*/
|
|
void stream_stop(struct context *cnt)
|
|
{
|
|
struct stream *list;
|
|
struct stream *next = cnt->stream.next;
|
|
|
|
MOTION_LOG(NTC, TYPE_STREAM, NO_ERRNO, "%s: Closing motion-stream listen socket"
|
|
" & active motion-stream sockets");
|
|
|
|
close(cnt->stream.socket);
|
|
cnt->stream.socket = -1;
|
|
|
|
while (next) {
|
|
list = next;
|
|
next = list->next;
|
|
|
|
if (list->tmpbuffer) {
|
|
free(list->tmpbuffer->ptr);
|
|
free(list->tmpbuffer);
|
|
}
|
|
|
|
close(list->socket);
|
|
free(list);
|
|
}
|
|
|
|
MOTION_LOG(NTC, TYPE_STREAM, NO_ERRNO, "%s: Closed motion-stream listen socket"
|
|
" & active motion-stream sockets");
|
|
}
|
|
|
|
/*
|
|
* stream_put
|
|
* Is the starting point of the stream loop. It is called from
|
|
* the motion_loop with the argument 'image' pointing to the latest frame.
|
|
* If config option 'stream_motion' is 'on' this function is called once
|
|
* per second (frame 0) and when Motion is detected excl pre_capture.
|
|
* If config option 'stream_motion' is 'off' this function is called once
|
|
* per captured picture frame.
|
|
* It is always run in setup mode for each picture frame captured and with
|
|
* the special setup image.
|
|
* The function does two things:
|
|
* It looks for possible waiting new clients and adds them.
|
|
* It sends latest picture frame to all connected clients.
|
|
* Note: Clients that have disconnected are handled in the stream_flush()
|
|
* function.
|
|
*/
|
|
void stream_put(struct context *cnt, unsigned char *image)
|
|
{
|
|
struct timeval timeout;
|
|
struct stream_buffer *tmpbuffer;
|
|
fd_set fdread;
|
|
int sl = cnt->stream.socket;
|
|
int sc;
|
|
/* Tthe following string has an extra 16 chars at end for length. */
|
|
const char jpeghead[] = "--BoundaryString\r\n"
|
|
"Content-type: image/jpeg\r\n"
|
|
"Content-Length: ";
|
|
int headlength = sizeof(jpeghead) - 1; /* Don't include terminator. */
|
|
char len[20]; /* Will be used for sprintf, must be >= 16 */
|
|
|
|
/*
|
|
* Timeout struct used to timeout the time we wait for a client
|
|
* and we do not wait at all.
|
|
*/
|
|
timeout.tv_sec = 0;
|
|
timeout.tv_usec = 0;
|
|
FD_ZERO(&fdread);
|
|
FD_SET(cnt->stream.socket, &fdread);
|
|
|
|
/*
|
|
* If we have not reached the max number of allowed clients per
|
|
* thread we will check to see if new clients are waiting to connect.
|
|
* If this is the case we add the client as a new stream struct and
|
|
* add this to the end of the chain of stream structs that are linked
|
|
* to each other.
|
|
*/
|
|
if ((cnt->stream_count < DEF_MAXSTREAMS) &&
|
|
(select(sl + 1, &fdread, NULL, NULL, &timeout) > 0)) {
|
|
sc = http_acceptsock(sl);
|
|
if (cnt->conf.stream_auth_method == 0) {
|
|
stream_add_client(&cnt->stream, sc);
|
|
cnt->stream_count++;
|
|
} else {
|
|
do_client_auth(cnt, sc);
|
|
}
|
|
}
|
|
|
|
/* Lock the mutex */
|
|
if (cnt->conf.stream_auth_method != 0)
|
|
pthread_mutex_lock(&stream_auth_mutex);
|
|
|
|
|
|
/* Call flush to send any previous partial-sends which are waiting. */
|
|
stream_flush(&cnt->stream, &cnt->stream_count, cnt->conf.stream_limit);
|
|
|
|
/* Check if any clients have available buffers. */
|
|
if (stream_check_write(&cnt->stream)) {
|
|
/*
|
|
* Yes - create a new tmpbuffer for current image.
|
|
* Note that this should create a buffer which is *much* larger
|
|
* than necessary, but it is difficult to estimate the
|
|
* minimum size actually required.
|
|
*/
|
|
tmpbuffer = stream_tmpbuffer(cnt->imgs.size);
|
|
|
|
/* Check if allocation was ok. */
|
|
if (tmpbuffer) {
|
|
int imgsize;
|
|
|
|
/*
|
|
* We need a pointer that points to the picture buffer
|
|
* just after the mjpeg header. We create a working pointer wptr
|
|
* to be used in the call to put_picture_memory which we can change
|
|
* and leave tmpbuffer->ptr intact.
|
|
*/
|
|
unsigned char *wptr = tmpbuffer->ptr;
|
|
|
|
/*
|
|
* For web protocol, our image needs to be preceded
|
|
* with a little HTTP, so we put that into the buffer
|
|
* first.
|
|
*/
|
|
memcpy(wptr, jpeghead, headlength);
|
|
|
|
/* Update our working pointer to point past header. */
|
|
wptr += headlength;
|
|
|
|
/* Create a jpeg image and place into tmpbuffer. */
|
|
tmpbuffer->size = put_picture_memory(cnt, wptr, cnt->imgs.size, image,
|
|
cnt->conf.stream_quality);
|
|
|
|
/* Fill in the image length into the header. */
|
|
imgsize = sprintf(len, "%9ld\r\n\r\n", tmpbuffer->size);
|
|
memcpy(wptr - imgsize, len, imgsize);
|
|
|
|
/* Append a CRLF for good measure. */
|
|
memcpy(wptr + tmpbuffer->size, "\r\n", 2);
|
|
|
|
/*
|
|
* Now adjust tmpbuffer->size to reflect the
|
|
* header at the beginning and the extra CRLF
|
|
* at the end.
|
|
*/
|
|
tmpbuffer->size += headlength + 2;
|
|
|
|
/*
|
|
* And finally put this buffer to all clients with
|
|
* no outstanding data from previous frames.
|
|
*/
|
|
stream_add_write(&cnt->stream, tmpbuffer, cnt->conf.stream_maxrate);
|
|
} else {
|
|
MOTION_LOG(ERR, TYPE_STREAM, SHOW_ERRNO, "%s: Error creating tmpbuffer");
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Now we call flush again. This time (assuming some clients were
|
|
* ready for the new frame) the new data will be written out.
|
|
*/
|
|
stream_flush(&cnt->stream, &cnt->stream_count, cnt->conf.stream_limit);
|
|
|
|
/* Unlock the mutex */
|
|
if (cnt->conf.stream_auth_method != 0)
|
|
pthread_mutex_unlock(&stream_auth_mutex);
|
|
|
|
return;
|
|
}
|