]> www.vanbest.org Git - motioneye-debian/commitdiff
added support for streaming authentication (HTTP digest, only supported
authorCalin Crisan <ccrisan@gmail.com>
Sat, 28 Feb 2015 14:00:19 +0000 (16:00 +0200)
committerCalin Crisan <ccrisan@gmail.com>
Sat, 28 Feb 2015 14:00:19 +0000 (16:00 +0200)
by newer motion versions)

src/config.py
src/handlers.py
src/mjpgclient.py
src/utils.py

index 8fac8ffac8bfdac4ee6c84e467aff3fd84c8d4a2..09a04e01a48d65d36655865478358069e712b7cf 100644 (file)
@@ -350,6 +350,10 @@ def set_camera(camera_id, camera_config):
                 camera_config['webcam_maxrate'] = camera_config.pop('stream_maxrate')
             if 'stream_localhost' in camera_config:
                 camera_config['webcam_localhost'] = camera_config.pop('stream_localhost')
+            if 'stream_auth_method' in camera_config:
+                camera_config.pop('stream_auth_method')
+            if 'stream_authentication' in camera_config:
+                camera_config.pop('stream_authentication')
             if 'event_gap' in camera_config:
                 camera_config['gap'] = camera_config.pop('event_gap')
             
@@ -529,6 +533,8 @@ def main_dict_to_ui(data):
 
 
 def camera_ui_to_dict(ui):
+    main_config = get_main() # needed for surveillance password
+
     data = {
         # device
         '@name': ui['name'],
@@ -558,7 +564,9 @@ def camera_ui_to_dict(ui):
         '@webcam_resolution': max(1, int(ui['streaming_resolution'])),
         '@webcam_server_resize': ui['streaming_server_resize'],
         'stream_motion': ui['streaming_motion'],
-        
+        'stream_auth_method': 2 if main_config['@normal_password'] else 0,
+        'stream_authentication': (main_config['@normal_username'] + ':' + main_config['@normal_password']) if main_config['@normal_password'] else '',
+
         # still images
         'output_pictures': False,
         'emulate_motion': False,
@@ -1361,6 +1369,7 @@ def _set_default_motion_camera(camera_id, data, old_motion=False):
         data.setdefault('stream_maxrate', 5)
         data.setdefault('stream_quality', 85)
         data.setdefault('stream_motion', False)
+        data.setdefault('stream_auth_method', 0)
 
     data.setdefault('@webcam_resolution', 100)
     data.setdefault('@webcam_server_resize', False)
index 8851bc6a1a9df11d1e84be21dc024bc1d9711545..d390fe39692b3619e28106c199393df036545563 100644 (file)
@@ -289,10 +289,12 @@ class ConfigHandler(BaseHandler):
             
             old_main_config = config.get_main()
             old_admin_credentials = '%s:%s' % (old_main_config.get('@admin_username', ''), old_main_config.get('@admin_password', ''))
-            
+            old_normal_credentials = '%s:%s' % (old_main_config.get('@normal_username', ''), old_main_config.get('@normal_password', ''))
+
             main_config = config.main_ui_to_dict(ui_config)
             main_config.setdefault('thread', old_main_config.get('thread', [])) 
             admin_credentials = '%s:%s' % (main_config.get('@admin_username', ''), main_config.get('@admin_password', ''))
+            normal_credentials = '%s:%s' % (main_config.get('@normal_username', ''), main_config.get('@normal_password', ''))
 
             additional_configs = config.get_additional_structure(camera=False)[1]           
             reboot_config_names = [('@_' + c['name']) for c in additional_configs.values() if c.get('reboot')]
@@ -302,18 +304,35 @@ class ConfigHandler(BaseHandler):
             config.set_main(main_config)
             
             reload = False
+            restart = False
             
             if admin_credentials != old_admin_credentials:
                 logging.debug('admin credentials changed, reload needed')
                 
                 reload = True
 
+            if normal_credentials != old_normal_credentials:
+                logging.debug('surveillance credentials changed, all camera configs must be updated')
+                
+                for camera_id in config.get_camera_ids():
+                    local_config = config.get_camera(camera_id)
+                    if not utils.local_camera(local_config):
+                        continue
+                    
+                    # this will update the stream authentication options
+                    ui_config = config.camera_dict_to_ui(local_config)
+                    local_config = config.camera_ui_to_dict(ui_config)
+                    config.set_camera(camera_id, local_config)
+                    
+                    restart = True
+
             if reboot and settings.ENABLE_REBOOT:
                 logging.debug('system settings changed, reboot needed')
-                
-                reboot = True
+        
+            else: 
+                reboot = False
 
-            return {'reload': reload, 'reboot': reboot}
+            return {'reload': reload, 'reboot': reboot, 'restart': restart}
         
         reload = False # indicates that browser should reload the page
         reboot = [False] # indicates that the server will reboot immediately
@@ -368,12 +387,17 @@ class ConfigHandler(BaseHandler):
                     
                     if so_far[0] >= len(ui_config): # finished
                         finish()
-        
-                for key, cfg in ui_config.items():
+
+                # make sure main config is handled first
+                items = ui_config.items()
+                items.sort(key=lambda (key, cfg): key != 'main')
+
+                for key, cfg in items:
                     if key == 'main':
                         result = set_main_config(cfg)
                         reload = result['reload'] or reload
                         reboot[0] = result['reboot'] or reboot[0]
+                        restart[0] = result['restart'] or restart[0]
                         check_finished(None, reload)
                         
                     else:
@@ -391,6 +415,7 @@ class ConfigHandler(BaseHandler):
             result = set_main_config(ui_config)
             reload = result['reload']
             reboot[0] = result['reboot']
+            restart[0] = result['restart']
 
     @BaseHandler.auth(admin=True)
     def set_preview(self, camera_id):
@@ -1330,6 +1355,4 @@ class LoginHandler(BaseHandler):
 
     def post(self):
         self.set_header('Content-Type', 'text/html')
-        if not self.current_user:
-            self.set_status(403)
         self.finish()
index 2c57ec7f237982bb4a4b04d069cb58a76148b718..c311aab05d5fb2b451fe97255efe8cdd860e99bb 100644 (file)
@@ -34,9 +34,12 @@ class MjpgClient(iostream.IOStream):
     last_jpg_moment = {} # dictionary of moments of the last received jpeg indexed by camera id
     last_access = {} # dictionary of access moments indexed by camera id
     
-    def __init__(self, camera_id, port):
+    def __init__(self, camera_id, port, username, password):
         self._camera_id = camera_id
         self._port = port
+        self._username = (username or '').encode('utf8')
+        self._password = (password or '').encode('utf8')
+        self._auth_digest_state = {}
         
         s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
         iostream.IOStream.__init__(self, s)
@@ -92,9 +95,51 @@ class MjpgClient(iostream.IOStream):
         logging.debug('mjpg client for camera %(camera_id)s connected on port %(port)s' % {
                 'port': self._port, 'camera_id': self._camera_id})
         
-        self.write(b"GET / HTTP/1.0\r\n\r\n")
-        self._seek_content_length()
+        self.write('GET / HTTP/1.0\r\n\r\n')
+        self._seek_http()
+    
+    def _seek_http(self):
+        if self._check_error():
+            return
+        
+        self.read_until_regex('HTTP/1.\d \d+ ', self._on_http)
+
+    def _on_http(self, data):
+        if data.endswith('401 '):
+            self._seek_www_authenticate()
+
+        else: # no authorization required, skip to content length
+            self._seek_content_length()
+
+    def _seek_www_authenticate(self):
+        if self._check_error():
+            return
+        
+        self.read_until('WWW-Authenticate:', self._on_before_www_authenticate)
+
+    def _on_before_www_authenticate(self, data):
+        if self._check_error():
+            return
         
+        self.read_until('\r\n', self._on_www_authenticate)
+    
+    def _on_www_authenticate(self, data):
+        if self._check_error():
+            return
+        
+        m = re.match('Digest\s*realm="([a-zA-Z0-9\-\s]+)",\s*nonce="([a-zA-Z0-9]+)"', data.strip())
+        if not m:
+            logging.error('mjpgclient: unknown authentication header: "%s"' % data)
+            return self._seek_content_length()
+
+        realm, nonce = m.groups()
+        self._auth_digest_state['realm'] = realm
+        self._auth_digest_state['nonce'] = nonce
+
+        auth_header = utils.build_digest_header('GET', '/', self._username, self._password, self._auth_digest_state)
+        self.write('GET / HTTP/1.0\r\n\r\nAuthorization: %s\r\n\r\n' % auth_header)
+        self._seek_http()
+
     def _seek_content_length(self):
         if self._check_error():
             return
@@ -196,7 +241,11 @@ def get_jpg(camera_id):
             return None
         
         port = camera_config['stream_port']
-        client = MjpgClient(camera_id, port)
+        username, password = None, None
+        if camera_config.get('stream_auth_method') == 2:
+            username, password = camera_config.get('stream_authentication', ':').split(':')
+
+        client = MjpgClient(camera_id, port, username, password)
         client.connect()
 
     MjpgClient.last_access[camera_id] = datetime.datetime.utcnow()
index b79b906bb090b32fb651eb67a9ad681a5644ad18..1ae4d9209ef9515ed8a35adc9ad9ff37ed992efa 100644 (file)
@@ -19,6 +19,7 @@ import datetime
 import hashlib
 import logging
 import os
+import time
 import urllib
 import urlparse
 
@@ -309,3 +310,96 @@ def compute_signature(method, uri, body, key):
     uri = urlparse.urlunsplit(parts)
     
     return hashlib.sha1('%s:%s:%s:%s' % (method, uri, body or '', key)).hexdigest().lower()
+
+
+def build_digest_header(method, url, username, password, state):
+    realm = state['realm']
+    nonce = state['nonce']
+    last_nonce = state.get('last_nonce', '')
+    nonce_count = state.get('nonce_count', 0)
+    qop = state.get('qop')
+    algorithm = state.get('algorithm')
+    opaque = state.get('opaque')
+
+    if algorithm is None:
+        _algorithm = 'MD5'
+
+    else:
+        _algorithm = algorithm.upper()
+
+    if _algorithm == 'MD5' or _algorithm == 'MD5-SESS':
+        def md5_utf8(x):
+            if isinstance(x, str):
+                x = x.encode('utf-8')
+            return hashlib.md5(x).hexdigest()
+        hash_utf8 = md5_utf8
+
+    elif _algorithm == 'SHA':
+        def sha_utf8(x):
+            if isinstance(x, str):
+                x = x.encode('utf-8')
+            return hashlib.sha1(x).hexdigest()
+        hash_utf8 = sha_utf8
+
+    KD = lambda s, d: hash_utf8("%s:%s" % (s, d))
+
+    if hash_utf8 is None:
+        return None
+
+    entdig = None
+    p_parsed = urlparse.urlparse(url)
+    path = p_parsed.path
+    if p_parsed.query:
+        path += '?' + p_parsed.query
+
+    A1 = '%s:%s:%s' % (username, realm, password)
+    A2 = '%s:%s' % (method, path)
+
+    HA1 = hash_utf8(A1)
+    HA2 = hash_utf8(A2)
+
+    if nonce == last_nonce:
+        nonce_count += 1
+
+    else:
+        nonce_count = 1
+    
+    ncvalue = '%08x' % nonce_count
+    s = str(nonce_count).encode('utf-8')
+    s += nonce.encode('utf-8')
+    s += time.ctime().encode('utf-8')
+    s += os.urandom(8)
+
+    cnonce = (hashlib.sha1(s).hexdigest()[:16])
+    if _algorithm == 'MD5-SESS':
+        HA1 = hash_utf8('%s:%s:%s' % (HA1, nonce, cnonce))
+
+    if qop is None:
+        respdig = KD(HA1, "%s:%s" % (nonce, HA2))
+    
+    elif qop == 'auth' or 'auth' in qop.split(','):
+        noncebit = "%s:%s:%s:%s:%s" % (
+            nonce, ncvalue, cnonce, 'auth', HA2
+            )
+        respdig = KD(HA1, noncebit)
+    
+    else:
+        return None
+
+    last_nonce = nonce
+
+    base = 'username="%s", realm="%s", nonce="%s", uri="%s", ' \
+           'response="%s"' % (username, realm, nonce, path, respdig)
+    if opaque:
+        base += ', opaque="%s"' % opaque
+    if algorithm:
+        base += ', algorithm="%s"' % algorithm
+    if entdig:
+        base += ', digest="%s"' % entdig
+    if qop:
+        base += ', qop="auth", nc=%s, cnonce="%s"' % (ncvalue, cnonce)
+    
+    state['last_nonce'] = last_nonce
+    state['nonce_count'] = nonce_count
+
+    return 'Digest %s' % (base)