mirror of
https://github.com/9001/copyparty.git
synced 2025-08-17 09:02:15 -06:00
fuse: support https + passwords, use argparse,
better handle windows trying to listdir(file)
This commit is contained in:
parent
7aba9cb76b
commit
90a5cb5e59
|
@ -19,6 +19,9 @@ dependencies:
|
||||||
+ on Linux: sudo apk add fuse
|
+ on Linux: sudo apk add fuse
|
||||||
+ on Macos: https://osxfuse.github.io/
|
+ on Macos: https://osxfuse.github.io/
|
||||||
+ on Windows: https://github.com/billziss-gh/winfsp/releases/latest
|
+ on Windows: https://github.com/billziss-gh/winfsp/releases/latest
|
||||||
|
|
||||||
|
get server cert:
|
||||||
|
awk '/-BEGIN CERTIFICATE-/ {a=1} a; /-END CERTIFICATE-/{exit}' <(openssl s_client -connect 127.0.0.1:3923 </dev/null 2>/dev/null) >cert.pem
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -31,6 +34,7 @@ import errno
|
||||||
import struct
|
import struct
|
||||||
import builtins
|
import builtins
|
||||||
import platform
|
import platform
|
||||||
|
import argparse
|
||||||
import threading
|
import threading
|
||||||
import traceback
|
import traceback
|
||||||
import http.client # py2: httplib
|
import http.client # py2: httplib
|
||||||
|
@ -39,11 +43,9 @@ from datetime import datetime
|
||||||
from urllib.parse import quote_from_bytes as quote
|
from urllib.parse import quote_from_bytes as quote
|
||||||
|
|
||||||
|
|
||||||
DEBUG = False # ctrl-f this to configure logging
|
|
||||||
|
|
||||||
|
|
||||||
WINDOWS = sys.platform == "win32"
|
WINDOWS = sys.platform == "win32"
|
||||||
MACOS = platform.system() == "Darwin"
|
MACOS = platform.system() == "Darwin"
|
||||||
|
info = log = dbg = None
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -138,22 +140,6 @@ class RecentLog(object):
|
||||||
print("".join(q), end="")
|
print("".join(q), end="")
|
||||||
|
|
||||||
|
|
||||||
if DEBUG:
|
|
||||||
# debug=on,
|
|
||||||
# windows terminals are slow (cmd.exe, mintty)
|
|
||||||
# otoh fancy_log beats RecentLog on linux
|
|
||||||
logger = RecentLog().put if WINDOWS else fancy_log
|
|
||||||
|
|
||||||
info = logger
|
|
||||||
log = logger
|
|
||||||
dbg = logger
|
|
||||||
else:
|
|
||||||
# debug=off, speed is dontcare
|
|
||||||
info = fancy_log
|
|
||||||
log = null_log
|
|
||||||
dbg = null_log
|
|
||||||
|
|
||||||
|
|
||||||
# [windows/cmd/cpy3] python dev\copyparty\bin\copyparty-fuse.py q: http://192.168.1.159:1234/
|
# [windows/cmd/cpy3] python dev\copyparty\bin\copyparty-fuse.py q: http://192.168.1.159:1234/
|
||||||
# [windows/cmd/msys2] C:\msys64\mingw64\bin\python3 dev\copyparty\bin\copyparty-fuse.py q: http://192.168.1.159:1234/
|
# [windows/cmd/msys2] C:\msys64\mingw64\bin\python3 dev\copyparty\bin\copyparty-fuse.py q: http://192.168.1.159:1234/
|
||||||
# [windows/mty/msys2] /mingw64/bin/python3 /c/Users/ed/dev/copyparty/bin/copyparty-fuse.py q: http://192.168.1.159:1234/
|
# [windows/mty/msys2] /mingw64/bin/python3 /c/Users/ed/dev/copyparty/bin/copyparty-fuse.py q: http://192.168.1.159:1234/
|
||||||
|
@ -195,10 +181,11 @@ class CacheNode(object):
|
||||||
|
|
||||||
|
|
||||||
class Gateway(object):
|
class Gateway(object):
|
||||||
def __init__(self, base_url):
|
def __init__(self, ar):
|
||||||
self.base_url = base_url
|
self.base_url = ar.base_url
|
||||||
|
self.password = ar.a
|
||||||
|
|
||||||
ui = urllib.parse.urlparse(base_url)
|
ui = urllib.parse.urlparse(self.base_url)
|
||||||
self.web_root = ui.path.strip("/")
|
self.web_root = ui.path.strip("/")
|
||||||
try:
|
try:
|
||||||
self.web_host, self.web_port = ui.netloc.split(":")
|
self.web_host, self.web_port = ui.netloc.split(":")
|
||||||
|
@ -208,10 +195,21 @@ class Gateway(object):
|
||||||
if ui.scheme == "http":
|
if ui.scheme == "http":
|
||||||
self.web_port = 80
|
self.web_port = 80
|
||||||
elif ui.scheme == "https":
|
elif ui.scheme == "https":
|
||||||
raise Exception("todo")
|
self.web_port = 443
|
||||||
else:
|
else:
|
||||||
raise Exception("bad url?")
|
raise Exception("bad url?")
|
||||||
|
|
||||||
|
self.ssl_context = None
|
||||||
|
self.use_tls = ui.scheme.lower() == "https"
|
||||||
|
if self.use_tls:
|
||||||
|
import ssl
|
||||||
|
|
||||||
|
if ar.td:
|
||||||
|
self.ssl_context = ssl._create_unverified_context()
|
||||||
|
elif ar.te:
|
||||||
|
self.ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS)
|
||||||
|
self.ssl_context.load_verify_locations(ar.te)
|
||||||
|
|
||||||
self.conns = {}
|
self.conns = {}
|
||||||
|
|
||||||
def quotep(self, path):
|
def quotep(self, path):
|
||||||
|
@ -226,7 +224,15 @@ class Gateway(object):
|
||||||
except:
|
except:
|
||||||
info("new conn [{}] [{}]".format(self.web_host, self.web_port))
|
info("new conn [{}] [{}]".format(self.web_host, self.web_port))
|
||||||
|
|
||||||
conn = http.client.HTTPConnection(self.web_host, self.web_port, timeout=260)
|
args = {}
|
||||||
|
if not self.use_tls:
|
||||||
|
C = http.client.HTTPConnection
|
||||||
|
else:
|
||||||
|
C = http.client.HTTPSConnection
|
||||||
|
if self.ssl_context:
|
||||||
|
args = {"context": self.ssl_context}
|
||||||
|
|
||||||
|
conn = C(self.web_host, self.web_port, timeout=260, **args)
|
||||||
|
|
||||||
self.conns[tid] = conn
|
self.conns[tid] = conn
|
||||||
return conn
|
return conn
|
||||||
|
@ -239,33 +245,53 @@ class Gateway(object):
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def sendreq(self, *args, **kwargs):
|
def sendreq(self, *args, headers={}, **kwargs):
|
||||||
tid = get_tid()
|
tid = get_tid()
|
||||||
|
if self.password:
|
||||||
|
headers["Cookie"] = "=".join(["cppwd", self.password])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
c = self.getconn(tid)
|
c = self.getconn(tid)
|
||||||
c.request(*list(args), **kwargs)
|
c.request(*list(args), headers=headers, **kwargs)
|
||||||
return c.getresponse()
|
return c.getresponse()
|
||||||
except:
|
except:
|
||||||
self.closeconn(tid)
|
dbg("bad conn")
|
||||||
|
|
||||||
|
self.closeconn(tid)
|
||||||
|
try:
|
||||||
c = self.getconn(tid)
|
c = self.getconn(tid)
|
||||||
c.request(*list(args), **kwargs)
|
c.request(*list(args), headers=headers, **kwargs)
|
||||||
return c.getresponse()
|
return c.getresponse()
|
||||||
|
except:
|
||||||
|
info("http connection failed:\n" + traceback.format_exc())
|
||||||
|
if self.use_tls and not self.ssl_context:
|
||||||
|
import ssl
|
||||||
|
|
||||||
|
cert = ssl.get_server_certificate((self.web_host, self.web_port))
|
||||||
|
info("server certificate probably not trusted:\n" + cert)
|
||||||
|
|
||||||
|
raise
|
||||||
|
|
||||||
def listdir(self, path):
|
def listdir(self, path):
|
||||||
web_path = self.quotep("/" + "/".join([self.web_root, path])) + "?dots"
|
web_path = self.quotep("/" + "/".join([self.web_root, path])) + "?dots"
|
||||||
r = self.sendreq("GET", web_path)
|
r = self.sendreq("GET", web_path)
|
||||||
if r.status != 200:
|
if r.status != 200:
|
||||||
self.closeconn()
|
self.closeconn()
|
||||||
raise Exception(
|
log(
|
||||||
"http error {} reading dir {} in {}".format(
|
"http error {} reading dir {} in {}".format(
|
||||||
r.status, web_path, rice_tid()
|
r.status, web_path, rice_tid()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
raise FuseOSError(errno.ENOENT)
|
||||||
|
|
||||||
|
if not r.getheader("Content-Type", "").startswith("text/html"):
|
||||||
|
log("listdir on file: {}".format(path))
|
||||||
|
raise FuseOSError(errno.ENOENT)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return self.parse_html(r)
|
return self.parse_html(r)
|
||||||
except:
|
except:
|
||||||
traceback.print_exc()
|
info(repr(path) + "\n" + traceback.format_exc())
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def download_file_range(self, path, ofs1, ofs2):
|
def download_file_range(self, path, ofs1, ofs2):
|
||||||
|
@ -358,11 +384,11 @@ class Gateway(object):
|
||||||
|
|
||||||
|
|
||||||
class CPPF(Operations):
|
class CPPF(Operations):
|
||||||
def __init__(self, base_url, dircache, filecache):
|
def __init__(self, ar):
|
||||||
self.gw = Gateway(base_url)
|
self.gw = Gateway(ar)
|
||||||
self.junk_fh_ctr = 3
|
self.junk_fh_ctr = 3
|
||||||
self.n_dircache = dircache
|
self.n_dircache = ar.cd
|
||||||
self.n_filecache = filecache
|
self.n_filecache = ar.cf
|
||||||
|
|
||||||
self.dircache = []
|
self.dircache = []
|
||||||
self.dircache_mtx = threading.Lock()
|
self.dircache_mtx = threading.Lock()
|
||||||
|
@ -799,7 +825,15 @@ class CPPF(Operations):
|
||||||
raise FuseOSError(errno.ENOENT)
|
raise FuseOSError(errno.ENOENT)
|
||||||
|
|
||||||
|
|
||||||
|
class TheArgparseFormatter(
|
||||||
|
argparse.RawTextHelpFormatter, argparse.ArgumentDefaultsHelpFormatter
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
global info, log, dbg
|
||||||
|
|
||||||
# filecache helps for reads that are ~64k or smaller;
|
# filecache helps for reads that are ~64k or smaller;
|
||||||
# linux generally does 128k so the cache is a slowdown,
|
# linux generally does 128k so the cache is a slowdown,
|
||||||
# windows likes to use 4k and 64k so cache is required,
|
# windows likes to use 4k and 64k so cache is required,
|
||||||
|
@ -811,26 +845,46 @@ def main():
|
||||||
# value is numSec until an entry goes stale
|
# value is numSec until an entry goes stale
|
||||||
nd = 1
|
nd = 1
|
||||||
|
|
||||||
try:
|
where = "local directory"
|
||||||
local, remote = sys.argv[1:3]
|
if WINDOWS:
|
||||||
filecache = nf if len(sys.argv) <= 3 else int(sys.argv[3])
|
where += " or DRIVE:"
|
||||||
dircache = nd if len(sys.argv) <= 4 else float(sys.argv[4])
|
|
||||||
except:
|
|
||||||
where = "local directory"
|
|
||||||
if WINDOWS:
|
|
||||||
where += " or DRIVE:"
|
|
||||||
|
|
||||||
print("need arg 1: " + where)
|
ex_pre = "\n " + os.path.basename(__file__) + " "
|
||||||
print("need arg 2: root url")
|
examples = ["http://192.168.1.69:3923/music/ ./music"]
|
||||||
print("optional 3: num files in filecache ({})".format(nf))
|
if WINDOWS:
|
||||||
print("optional 4: num seconds / dircache ({})".format(nd))
|
examples.append("http://192.168.1.69:3923/music/ M:")
|
||||||
print()
|
|
||||||
print("example:")
|
|
||||||
print(" copyparty-fuse.py ./music http://192.168.1.69:3923/music/")
|
|
||||||
if WINDOWS:
|
|
||||||
print(" copyparty-fuse.py M: http://192.168.1.69:3923/music/")
|
|
||||||
|
|
||||||
return
|
ap = argparse.ArgumentParser(
|
||||||
|
formatter_class=TheArgparseFormatter,
|
||||||
|
epilog="example:" + ex_pre + ex_pre.join(examples),
|
||||||
|
)
|
||||||
|
ap.add_argument(
|
||||||
|
"-cd", metavar="NUM_SECONDS", type=float, default=nd, help="directory cache"
|
||||||
|
)
|
||||||
|
ap.add_argument(
|
||||||
|
"-cf", metavar="NUM_BLOCKS", type=int, default=nf, help="file cache"
|
||||||
|
)
|
||||||
|
ap.add_argument("-a", metavar="PASSWORD", help="password")
|
||||||
|
ap.add_argument("-d", action="store_true", help="enable debug")
|
||||||
|
ap.add_argument("-te", metavar="PEM_FILE", help="certificate to expect/verify")
|
||||||
|
ap.add_argument("-td", action="store_true", help="disable certificate check")
|
||||||
|
ap.add_argument("base_url", type=str, help="remote copyparty URL to mount")
|
||||||
|
ap.add_argument("local_path", type=str, help=where + " to mount it on")
|
||||||
|
ar = ap.parse_args()
|
||||||
|
|
||||||
|
if ar.d:
|
||||||
|
# windows terminals are slow (cmd.exe, mintty)
|
||||||
|
# otoh fancy_log beats RecentLog on linux
|
||||||
|
logger = RecentLog().put if WINDOWS else fancy_log
|
||||||
|
|
||||||
|
info = logger
|
||||||
|
log = logger
|
||||||
|
dbg = logger
|
||||||
|
else:
|
||||||
|
# debug=off, speed is dontcare
|
||||||
|
info = fancy_log
|
||||||
|
log = null_log
|
||||||
|
dbg = null_log
|
||||||
|
|
||||||
if WINDOWS:
|
if WINDOWS:
|
||||||
os.system("")
|
os.system("")
|
||||||
|
@ -845,7 +899,7 @@ def main():
|
||||||
if not MACOS:
|
if not MACOS:
|
||||||
args["nonempty"] = True
|
args["nonempty"] = True
|
||||||
|
|
||||||
FUSE(CPPF(remote, dircache, filecache), local, **args)
|
FUSE(CPPF(ar), ar.local_path, **args)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
Loading…
Reference in a new issue