Add verified camera settings, family version recovery and account self service
This commit is contained in:
@@ -3,6 +3,8 @@ import base64
|
||||
import concurrent.futures
|
||||
import datetime as dt
|
||||
import hashlib
|
||||
import functools
|
||||
import camera_settings
|
||||
import hmac
|
||||
import http.client
|
||||
import http.cookies
|
||||
@@ -126,6 +128,8 @@ def integer(value, lo, hi):
|
||||
|
||||
def host_address(value):
|
||||
try:
|
||||
if not isinstance(value, str):
|
||||
raise ValueError()
|
||||
ip = ipaddress.ip_address(value)
|
||||
if ip.version != 4 or ip.is_global or ip.is_loopback or ip.is_multicast or ip.is_unspecified or ip.is_reserved:
|
||||
raise ValueError()
|
||||
@@ -138,10 +142,28 @@ SITE_FIELDS = ('name', 'province', 'city', 'district', 'street', 'community', 'o
|
||||
'building', 'unit', 'floor', 'door', 'address', 'note')
|
||||
|
||||
|
||||
def serialized_save(function):
|
||||
@functools.wraps(function)
|
||||
def save(body):
|
||||
with LOCK:
|
||||
return function(body)
|
||||
return save
|
||||
|
||||
|
||||
def object_key(table, body):
|
||||
key = body.get('id', '')
|
||||
if key in ('', None):
|
||||
return secrets.token_hex(8)
|
||||
if not isinstance(key, str) or not re.fullmatch('[a-f0-9]{16}', key):
|
||||
raise Problem('对象编号不正确')
|
||||
if not get_object(table, key):
|
||||
raise Problem('对象不存在,请重新载入', 404)
|
||||
return key
|
||||
|
||||
|
||||
@serialized_save
|
||||
def save_site(body):
|
||||
key = body.get('id') or secrets.token_hex(8)
|
||||
if not re.fullmatch('[a-f0-9]{16}', key):
|
||||
raise Problem('空间编号不正确')
|
||||
key = object_key('sites', body)
|
||||
item = {k: clean_text(body.get(k, ''), 240 if k in ('address', 'note') else 100,
|
||||
required=k == 'name') for k in SITE_FIELDS}
|
||||
item.update(id=key, kind=body.get('kind', 'home'))
|
||||
@@ -152,10 +174,9 @@ def save_site(body):
|
||||
return item
|
||||
|
||||
|
||||
@serialized_save
|
||||
def save_camera(body):
|
||||
key = body.get('id') or secrets.token_hex(8)
|
||||
if not re.fullmatch('[a-f0-9]{16}', key):
|
||||
raise Problem('设备编号不正确')
|
||||
key = object_key('cameras', body)
|
||||
old = get_object('cameras', key) or {}
|
||||
item = {k: clean_text(body.get(k, ''), 120, required=k == 'name')
|
||||
for k in ('name', 'point', 'username', 'model')}
|
||||
@@ -202,10 +223,9 @@ def save_camera(body):
|
||||
return public_camera(item)
|
||||
|
||||
|
||||
@serialized_save
|
||||
def save_asset(body):
|
||||
key = body.get('id') or secrets.token_hex(8)
|
||||
if not re.fullmatch('[a-f0-9]{16}', key):
|
||||
raise Problem('对象编号不正确')
|
||||
key = object_key('assets', body)
|
||||
site = body.get('siteId')
|
||||
if not get_object('sites', site):
|
||||
raise Problem('请选择所属空间')
|
||||
@@ -215,19 +235,18 @@ def save_asset(body):
|
||||
attached = [c for c in objects('cameras') if c.get('assetId') == key]
|
||||
if len(attached) > item['lensCount']:
|
||||
raise Problem('镜头数不能小于已经建立的通道数')
|
||||
with LOCK:
|
||||
save_object('assets', item)
|
||||
with DB:
|
||||
DB.execute('INSERT OR REPLACE INTO assets VALUES (?,?)', (key, json.dumps(item)))
|
||||
for c in attached:
|
||||
c['siteId'] = site
|
||||
save_object('cameras', c)
|
||||
DB.execute('UPDATE cameras SET body=? WHERE id=?', (json.dumps(c), c['id']))
|
||||
audit('保存摄像头对象', item['name'])
|
||||
return item
|
||||
|
||||
|
||||
@serialized_save
|
||||
def save_recorder(body):
|
||||
key = body.get('id') or secrets.token_hex(8)
|
||||
if not re.fullmatch('[a-f0-9]{16}', key):
|
||||
raise Problem('录像机编号不正确')
|
||||
key = object_key('recorders', body)
|
||||
old = get_object('recorders', key) or {}
|
||||
item = {k: clean_text(body.get(k, ''), 120, required=k == 'name')
|
||||
for k in ('name', 'brand', 'model', 'username')}
|
||||
@@ -249,7 +268,13 @@ def save_recorder(body):
|
||||
|
||||
|
||||
def public_camera(c):
|
||||
return {**{k: v for k, v in c.items() if k != 'password'}, 'passwordConfigured': bool(c['password'])}
|
||||
result = {**{k: v for k, v in c.items() if k != 'password'}, 'passwordConfigured': bool(c.get('password'))}
|
||||
if c.get('sourceKind') == 'recorder':
|
||||
recorder = get_object('recorders', c.get('recorderId'))
|
||||
if recorder:
|
||||
result.update({k: recorder[k] for k in ('host', 'port', 'username')})
|
||||
result['passwordConfigured'] = bool(recorder.get('password'))
|
||||
return result
|
||||
|
||||
|
||||
def stream_name(c, quality='main'):
|
||||
@@ -512,8 +537,14 @@ class Handler(BaseHTTPRequestHandler):
|
||||
|
||||
def validate_origin(self):
|
||||
origin = self.headers.get('Origin')
|
||||
if origin and url.urlsplit(origin).netloc != self.headers.get('Host'):
|
||||
raise Problem('请从系统页面提交操作', 403)
|
||||
if origin:
|
||||
try:
|
||||
parsed = url.urlsplit(origin)
|
||||
valid = parsed.scheme in ('http', 'https') and parsed.netloc == self.headers.get('Host')
|
||||
except ValueError:
|
||||
valid = False
|
||||
if not valid:
|
||||
raise Problem('请从系统页面提交操作', 403)
|
||||
|
||||
def login(self, data, setup=False):
|
||||
ip = self.client_address[0]
|
||||
@@ -564,13 +595,29 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self.login(data, path == '/api/setup')
|
||||
return
|
||||
self.require_auth()
|
||||
if path not in ('/api/logout', '/api/family/person', '/api/family/link', '/api/family/event', '/api/ptz'):
|
||||
if path not in ('/api/logout', '/api/account/password', '/api/family/restore',
|
||||
'/api/family/person', '/api/family/link', '/api/family/event', '/api/ptz'):
|
||||
self.require_admin()
|
||||
if path == '/api/logout':
|
||||
with LOCK:
|
||||
DB.execute('DELETE FROM sessions WHERE hash=?', (hashlib.sha256(self.token().encode()).hexdigest(),))
|
||||
DB.commit()
|
||||
self.answer({'ok': True}, cookie='vision=; Path=/; Max-Age=0; HttpOnly; SameSite=Strict')
|
||||
elif path == '/api/account/password':
|
||||
result = ACCOUNTS.change_password(data, self.user)
|
||||
self.answer(result, cookie='vision=; Path=/; Max-Age=0; HttpOnly; SameSite=Strict')
|
||||
elif path == '/api/family/restore':
|
||||
self.answer(FAMILY.restore(data, self.user))
|
||||
elif path == '/api/camera/settings':
|
||||
c = ACCOUNTS.camera(self.user, get_object('cameras', data.get('camera')))
|
||||
try:
|
||||
result = camera_settings.CONTROLLER.save(c, data)
|
||||
except ValueError as e:
|
||||
raise Problem(str(e))
|
||||
except Exception:
|
||||
raise Problem('设备保存请求未确认,可能已经生效;请重新读取参数后核对,勿反复提交', 502)
|
||||
audit('修改摄像机参数 · ' + data['section'], c['name'] + ' / ' + self.user['username'])
|
||||
self.answer(result)
|
||||
elif path == '/api/ptz':
|
||||
c = ACCOUNTS.control(self.user, get_object('cameras', data.get('camera')))
|
||||
try:
|
||||
@@ -741,6 +788,23 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self.answer(result)
|
||||
elif path == '/api/family':
|
||||
self.answer(FAMILY.snapshot(self.user))
|
||||
elif path == '/api/camera/settings':
|
||||
self.require_admin()
|
||||
c = ACCOUNTS.camera(self.user, get_object('cameras', query.get('camera')))
|
||||
try:
|
||||
result, _ = camera_settings.CONTROLLER.read(c, query.get('quality', 'main'))
|
||||
except ValueError as e:
|
||||
raise Problem(str(e))
|
||||
except Exception:
|
||||
raise Problem('读取摄像机参数失败,请检查连接、设备账号或 ONVIF 服务', 502)
|
||||
self.answer(result)
|
||||
elif path in ('/api/family/history', '/api/family/change'):
|
||||
try:
|
||||
number = int(query['before' if path.endswith('history') else 'revision']) if ('before' if path.endswith('history') else 'revision') in query else None
|
||||
except ValueError:
|
||||
raise Problem('修改记录编号不正确')
|
||||
self.answer(FAMILY.history(self.user, query.get('kind'), query.get('id'), number)
|
||||
if path.endswith('history') else FAMILY.change(self.user, number))
|
||||
elif path == '/api/audit':
|
||||
self.require_admin()
|
||||
with LOCK:
|
||||
@@ -791,6 +855,8 @@ class Handler(BaseHTTPRequestHandler):
|
||||
'/family-graph.js': 'family-graph.js', '/family-map.js': 'family-map.js',
|
||||
'/family-map.css': 'family-map.css',
|
||||
'/family-events.js': 'family-events.js',
|
||||
'/family-history.js': 'family-history.js',
|
||||
'/camera-settings.js': 'camera-settings.js',
|
||||
'/monitor.js': 'monitor.js', '/calendar.js': 'calendar.js', '/date-fields.js': 'date-fields.js',
|
||||
'/zhao-icon.png': 'zhao-icon.png',
|
||||
'/vendor/hls.min.js': 'vendor/hls.min.js'}
|
||||
|
||||
Reference in New Issue
Block a user