mirror of
https://github.com/9001/copyparty.git
synced 2026-04-12 15:22:32 -06:00
dna scaffold
This commit is contained in:
parent
fb5384f412
commit
a3ce1ecde5
|
|
@ -1452,6 +1452,14 @@ def add_zc_ssdp(ap):
|
|||
ap2.add_argument("--zsl", metavar="PATH", type=u, default="/?hc", help="location to include in the url (or a complete external URL), for example [\033[32mpriv/?pw=hunter2\033[0m] (goes directly to /priv/ with password hunter2) or [\033[32m?hc=priv&pw=hunter2\033[0m] (shows mounting options for /priv/ with password)")
|
||||
ap2.add_argument("--zsid", metavar="UUID", type=u, default=zsid, help="USN (device identifier) to announce")
|
||||
|
||||
def add_zc_dlna(ap):
|
||||
ap2 = ap.add_argument_group("DLNA options")
|
||||
ap2.add_argument("--zd", action="store_true", help="announce MediaServer:1 over SSDP for DLNA device support")
|
||||
# ap2.add_argument("--dn-on", metavar="NETS", type=u, default="", help="enable SSDP ONLY on the comma-separated list of subnets and/or interface names/indexes")
|
||||
# ap2.add_argument("--dn-off", metavar="NETS", type=u, default="", help="disable SSDP on the comma-separated list of subnets and/or interface names/indexes")
|
||||
ap2.add_argument("--zdv", action="store_true", help="verbose DLNA")
|
||||
# ap2.add_argument("--dnl", metavar="PATH", type=u, default="/?hc", help="location to include in the url (or a complete external URL), for example [\033[32mpriv/?pw=hunter2\033[0m] (goes directly to /priv/ with password hunter2) or [\033[32m?hc=priv&pw=hunter2\033[0m] (shows mounting options for /priv/ with password)")
|
||||
# ap2.add_argument("--dnid", metavar="UUID", type=u, default=zsid, help="USN (device identifier) to announce")
|
||||
|
||||
def add_sftp(ap):
|
||||
ap2 = ap.add_argument_group("SFTP options")
|
||||
|
|
@ -1990,6 +1998,7 @@ def run_argparse(
|
|||
add_zeroconf(ap)
|
||||
add_zc_mdns(ap)
|
||||
add_zc_ssdp(ap)
|
||||
add_zc_dlna(ap)
|
||||
add_fs(ap)
|
||||
add_share(ap)
|
||||
add_upload(ap)
|
||||
|
|
|
|||
240
copyparty/dlna.py
Normal file
240
copyparty/dlna.py
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import errno
|
||||
import re
|
||||
import select
|
||||
import socket
|
||||
import time
|
||||
|
||||
from .__init__ import TYPE_CHECKING
|
||||
from .multicast import MC_Sck, MCast
|
||||
from .util import CachedSet, formatdate, html_escape, min_ex
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .broker_util import BrokerCli
|
||||
from .httpcli import HttpCli
|
||||
from .svchub import SvcHub
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
from typing import Optional, Union
|
||||
|
||||
|
||||
GRP = "239.255.255.250"
|
||||
|
||||
|
||||
class DLNA_Sck(MC_Sck):
|
||||
def __init__(self, *a):
|
||||
super(DLNA_Sck, self).__init__(*a)
|
||||
self.hport = 0
|
||||
|
||||
|
||||
class DLNAr(object):
|
||||
"""generates http responses for httpcli"""
|
||||
|
||||
def __init__(self, broker: "BrokerCli") -> None:
|
||||
self.broker = broker
|
||||
self.args = broker.args
|
||||
|
||||
def reply(self, hc: "HttpCli") -> bool:
|
||||
if hc.vpath.endswith("device.xml"):
|
||||
return self.tx_device(hc)
|
||||
|
||||
hc.reply(b"unknown request", 400)
|
||||
return False
|
||||
|
||||
def tx_device(self, hc: "HttpCli") -> bool:
|
||||
zs = """
|
||||
<?xml version="1.0"?>
|
||||
<root xmlns="urn:schemas-upnp-org:device-1-0">
|
||||
<specVersion>
|
||||
<major>1</major>
|
||||
<minor>0</minor>
|
||||
</specVersion>
|
||||
<URLBase>{}</URLBase>
|
||||
<device>
|
||||
<presentationURL>{}</presentationURL>
|
||||
<deviceType>urn:schemas-upnp-org:device:Basic:1</deviceType>
|
||||
<friendlyName>{}</friendlyName>
|
||||
<modelDescription>file server</modelDescription>
|
||||
<manufacturer>ed</manufacturer>
|
||||
<manufacturerURL>https://ocv.me/</manufacturerURL>
|
||||
<modelName>copyparty</modelName>
|
||||
<modelURL>https://github.com/9001/copyparty/</modelURL>
|
||||
<UDN>{}</UDN>
|
||||
<serviceList>
|
||||
<service>
|
||||
<serviceType>urn:schemas-upnp-org:device:Basic:1</serviceType>
|
||||
<serviceId>urn:schemas-upnp-org:device:Basic</serviceId>
|
||||
<controlURL>/.cpr/dlna/services.xml</controlURL>
|
||||
<eventSubURL>/.cpr/dlna/services.xml</eventSubURL>
|
||||
<SCPDURL>/.cpr/dlna/services.xml</SCPDURL>
|
||||
</service>
|
||||
</serviceList>
|
||||
</device>
|
||||
</root>"""
|
||||
|
||||
c = html_escape
|
||||
sip, sport = hc.s.getsockname()[:2]
|
||||
sip = sip.replace("::ffff:", "")
|
||||
proto = "https" if self.args.https_only else "http"
|
||||
ubase = "{}://{}:{}".format(proto, sip, sport)
|
||||
zsl = self.args.zsl
|
||||
url = zsl if "://" in zsl else ubase + "/" + zsl.lstrip("/")
|
||||
name = self.args.doctitle
|
||||
zs = zs.strip().format(c(ubase), c(url), c(name), c(self.args.zsid))
|
||||
hc.reply(zs.encode("utf-8", "replace"))
|
||||
return False # close connection
|
||||
|
||||
|
||||
class DLNAd(MCast):
|
||||
"""communicates with dlna clients over multicast"""
|
||||
|
||||
def __init__(self, hub: "SvcHub", ngen: int) -> None:
|
||||
al = hub.args
|
||||
vinit = al.zsv and not al.zmv
|
||||
super(DLNAd, self).__init__(
|
||||
hub, DLNA_Sck, al.zs_on, al.zs_off, GRP, "", 1900, vinit
|
||||
)
|
||||
self.srv: dict[socket.socket, DLNA_Sck] = {}
|
||||
self.logsrc = "DLNA-{}".format(ngen)
|
||||
self.ngen = ngen
|
||||
|
||||
self.rxc = CachedSet(0.7)
|
||||
self.txc = CachedSet(5) # win10: every 3 sec
|
||||
self.ptn_st = re.compile(b"\nst: *upnp:rootdevice", re.I)
|
||||
|
||||
def log(self, msg: str, c: Union[int, str] = 0) -> None:
|
||||
self.log_func(self.logsrc, msg, c)
|
||||
|
||||
def run(self) -> None:
|
||||
try:
|
||||
bound = self.create_servers()
|
||||
except:
|
||||
t = "no server IP matches the dlna config\n{}"
|
||||
self.log(t.format(min_ex()), 1)
|
||||
bound = []
|
||||
|
||||
if not bound:
|
||||
self.log("failed to announce copyparty services on the network", 3)
|
||||
return
|
||||
|
||||
# find http port for this listening ip
|
||||
for srv in self.srv.values():
|
||||
tcps = self.hub.tcpsrv.bound
|
||||
hp = next((x[1] for x in tcps if x[0] in ("0.0.0.0", srv.ip)), 0)
|
||||
hp = hp or next((x[1] for x in tcps if x[0] == "::"), 0)
|
||||
if not hp:
|
||||
hp = tcps[0][1]
|
||||
self.log("assuming port {} for {}".format(hp, srv.ip), 3)
|
||||
srv.hport = hp
|
||||
|
||||
self.log("listening")
|
||||
try:
|
||||
self.run2()
|
||||
except OSError as ex:
|
||||
if ex.errno != errno.EBADF:
|
||||
raise
|
||||
|
||||
self.log("stopping due to {}".format(ex), "90")
|
||||
|
||||
self.log("stopped", 2)
|
||||
|
||||
def run2(self) -> None:
|
||||
try:
|
||||
if self.args.no_poll:
|
||||
raise Exception()
|
||||
fd2sck = {}
|
||||
srvpoll = select.poll()
|
||||
for sck in self.srv:
|
||||
fd = sck.fileno()
|
||||
fd2sck[fd] = sck
|
||||
srvpoll.register(fd, select.POLLIN)
|
||||
except Exception as ex:
|
||||
srvpoll = None
|
||||
if not self.args.no_poll:
|
||||
t = "WARNING: failed to poll(), will use select() instead: %r"
|
||||
self.log(t % (ex,), 3)
|
||||
|
||||
while self.running:
|
||||
if srvpoll:
|
||||
pr = srvpoll.poll((self.args.z_chk or 180) * 1000)
|
||||
rx = [fd2sck[x[0]] for x in pr if x[1] & select.POLLIN]
|
||||
else:
|
||||
rdy = select.select(self.srv, [], [], self.args.z_chk or 180)
|
||||
rx: list[socket.socket] = rdy[0] # type: ignore
|
||||
|
||||
self.rxc.cln()
|
||||
buf = b""
|
||||
addr = ("0", 0)
|
||||
for sck in rx:
|
||||
try:
|
||||
buf, addr = sck.recvfrom(4096)
|
||||
self.eat(buf, addr)
|
||||
except:
|
||||
if not self.running:
|
||||
break
|
||||
|
||||
t = "{} {} \033[33m|{}| {}\n{}".format(
|
||||
self.srv[sck].name, addr, len(buf), repr(buf)[2:-1], min_ex()
|
||||
)
|
||||
self.log(t, 6)
|
||||
|
||||
def stop(self) -> None:
|
||||
self.running = False
|
||||
for srv in self.srv.values():
|
||||
try:
|
||||
srv.sck.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
self.srv.clear()
|
||||
|
||||
def eat(self, buf: bytes, addr: tuple[str, int]) -> None:
|
||||
cip = addr[0]
|
||||
if cip.startswith("169.254") and not self.ll_ok:
|
||||
return
|
||||
|
||||
if buf in self.rxc.c:
|
||||
return
|
||||
|
||||
srv: Optional[DLNA_Sck] = self.map_client(cip) # type: ignore
|
||||
if not srv:
|
||||
return
|
||||
|
||||
self.rxc.add(buf)
|
||||
if not buf.startswith(b"M-SEARCH * HTTP/1."):
|
||||
return
|
||||
|
||||
if not self.ptn_st.search(buf):
|
||||
return
|
||||
|
||||
if self.args.zsv:
|
||||
t = "{} [{}] \033[36m{} \033[0m|{}|"
|
||||
self.log(t.format(srv.name, srv.ip, cip, len(buf)), "90")
|
||||
|
||||
zs = """
|
||||
HTTP/1.1 200 OK
|
||||
CACHE-CONTROL: max-age=1800
|
||||
DATE: {0}
|
||||
EXT:
|
||||
LOCATION: http://{1}:{2}/.cpr/dlna/device.xml
|
||||
OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01
|
||||
01-NLS: {3}
|
||||
SERVER: UPnP/1.0
|
||||
ST: upnp:rootdevice
|
||||
USN: {3}::upnp:rootdevice
|
||||
BOOTID.UPNP.ORG: 0
|
||||
CONFIGID.UPNP.ORG: 1
|
||||
|
||||
"""
|
||||
v4 = srv.ip.replace("::ffff:", "")
|
||||
zs = zs.format(formatdate(), v4, srv.hport, self.args.zsid)
|
||||
zb = zs[1:].replace("\n", "\r\n").encode("utf-8", "replace")
|
||||
srv.sck.sendto(zb, addr[:2])
|
||||
|
||||
if cip not in self.txc.c:
|
||||
self.log("{} [{}] --> {}".format(srv.name, srv.ip, cip), 6)
|
||||
|
||||
self.txc.add(cip)
|
||||
self.txc.cln()
|
||||
|
|
@ -1382,6 +1382,13 @@ class HttpCli(object):
|
|||
self.reply(b"ssdp is disabled in server config", 404)
|
||||
return False
|
||||
|
||||
if self.vpath.startswith(".cpr/dlna"):
|
||||
if self.conn.hsrv.dlna:
|
||||
return self.conn.hsrv.dlna.reply(self)
|
||||
else:
|
||||
self.reply(b"dlna is disabled in server config", 404)
|
||||
return False
|
||||
|
||||
if self.vpath == ".cpr/metrics":
|
||||
return self.conn.hsrv.metrics.tx(self)
|
||||
|
||||
|
|
@ -2280,6 +2287,14 @@ class HttpCli(object):
|
|||
except:
|
||||
raise Pebkac(400, "client d/c before 100 continue")
|
||||
|
||||
# DLNA/SSDP control requests (SOAP)
|
||||
if self.vpath.startswith(".cpr/dlna/ctl/"):
|
||||
if self.conn.hsrv.dlna:
|
||||
return self.conn.hsrv.dlna.reply(self)
|
||||
else:
|
||||
self.reply(b"dlna is disabled in server config", 404)
|
||||
return False
|
||||
|
||||
if "raw" in self.uparam:
|
||||
return self.handle_stash(False)
|
||||
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ if TYPE_CHECKING:
|
|||
from .authsrv import VFS
|
||||
from .broker_util import BrokerCli
|
||||
from .ssdp import SSDPr
|
||||
from .dlna import DLNAr
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
from typing import Any, Optional
|
||||
|
|
@ -129,6 +130,7 @@ class HttpSrv(object):
|
|||
self.magician = Magician()
|
||||
self.nm = NetMap([], [])
|
||||
self.ssdp: Optional["SSDPr"] = None
|
||||
self.dlna: Optional["DLNAr"] = None
|
||||
self.gpwd = Garda(self.args.ban_pw)
|
||||
self.gpwc = Garda(self.args.ban_pwc)
|
||||
self.g404 = Garda(self.args.ban_404)
|
||||
|
|
@ -226,6 +228,11 @@ class HttpSrv(object):
|
|||
|
||||
self.ssdp = SSDPr(broker)
|
||||
|
||||
if getattr(self.args, "zd", False):
|
||||
from .dlna import DLNAr
|
||||
|
||||
self.dlna = DLNAr(broker)
|
||||
|
||||
if self.tp_q:
|
||||
self.start_threads(4)
|
||||
|
||||
|
|
|
|||
|
|
@ -93,6 +93,8 @@ if TYPE_CHECKING:
|
|||
try:
|
||||
from .mdns import MDNS
|
||||
from .ssdp import SSDPd
|
||||
from .dlna import DLNAd
|
||||
from .dlna import DLNAd
|
||||
except:
|
||||
pass
|
||||
|
||||
|
|
@ -477,6 +479,7 @@ class SvcHub(object):
|
|||
self.zc_ngen = 0
|
||||
self.mdns: Optional["MDNS"] = None
|
||||
self.ssdp: Optional["SSDPd"] = None
|
||||
self.dlna: Optional["DLNAd"] = None
|
||||
|
||||
# decide which worker impl to use
|
||||
if self.check_mp_enable():
|
||||
|
|
@ -1039,7 +1042,7 @@ class SvcHub(object):
|
|||
have_tcp = True
|
||||
if not have_tcp:
|
||||
zb = False
|
||||
zs = "z zm zm4 zm6 zmv zmvv zs zsv zv"
|
||||
zs = "z zm zm4 zm6 zmv zmvv zs zsv zv zd zdv"
|
||||
for zs in zs.split():
|
||||
if getattr(al, zs, False):
|
||||
setattr(al, zs, False)
|
||||
|
|
@ -1468,6 +1471,18 @@ class SvcHub(object):
|
|||
except:
|
||||
self.log("root", "ssdp startup failed;\n" + min_ex(), 3)
|
||||
|
||||
if getattr(self.args, "zd", False):
|
||||
try:
|
||||
from .dlna import DLNAd
|
||||
|
||||
if self.dlna:
|
||||
self.dlna.stop()
|
||||
|
||||
self.dlna = DLNAd(self, self.zc_ngen)
|
||||
Daemon(self.dlna.run, "dlna")
|
||||
except:
|
||||
self.log("root", "dlna startup failed;\n" + min_ex(), 3)
|
||||
|
||||
def reload(self, rescan_all_vols: bool, up2k: bool) -> str:
|
||||
t = "users, volumes, and volflags have been reloaded"
|
||||
with self.reload_mutex:
|
||||
|
|
@ -1566,6 +1581,10 @@ class SvcHub(object):
|
|||
tasks.append(Daemon(self.ssdp.stop, "ssdp"))
|
||||
slp = time.time() + 0.5
|
||||
|
||||
if self.dlna:
|
||||
tasks.append(Daemon(self.dlna.stop, "dlna"))
|
||||
slp = time.time() + 0.5
|
||||
|
||||
self.broker.shutdown()
|
||||
self.tcpsrv.shutdown()
|
||||
self.up2k.shutdown()
|
||||
|
|
|
|||
|
|
@ -423,8 +423,10 @@ IMPLICATIONS = [
|
|||
["smbv", "smb"],
|
||||
["zv", "zmv"],
|
||||
["zv", "zsv"],
|
||||
["zv", "zdv"],
|
||||
["z", "zm"],
|
||||
["z", "zs"],
|
||||
["z", "zd"],
|
||||
["zmvv", "zmv"],
|
||||
["zm4", "zm"],
|
||||
["zm6", "zm"],
|
||||
|
|
|
|||
Loading…
Reference in a new issue