copyparty 1.10.2__py3-none-any.whl → 1.11.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. copyparty/__main__.py +16 -6
  2. copyparty/__version__.py +3 -3
  3. copyparty/authsrv.py +365 -66
  4. copyparty/cfg.py +3 -0
  5. copyparty/ftpd.py +2 -2
  6. copyparty/httpcli.py +113 -48
  7. copyparty/httpconn.py +4 -1
  8. copyparty/httpsrv.py +8 -3
  9. copyparty/metrics.py +3 -0
  10. copyparty/multicast.py +1 -1
  11. copyparty/smbd.py +1 -1
  12. copyparty/svchub.py +55 -14
  13. copyparty/tftpd.py +11 -9
  14. copyparty/up2k.py +143 -49
  15. copyparty/util.py +52 -8
  16. copyparty/web/baguettebox.js.gz +0 -0
  17. copyparty/web/browser.css.gz +0 -0
  18. copyparty/web/browser.html +1 -1
  19. copyparty/web/browser.js.gz +0 -0
  20. copyparty/web/browser2.html +1 -1
  21. copyparty/web/deps/easymde.css.gz +0 -0
  22. copyparty/web/deps/easymde.js.gz +0 -0
  23. copyparty/web/md.css.gz +0 -0
  24. copyparty/web/md.html +1 -1
  25. copyparty/web/md.js.gz +0 -0
  26. copyparty/web/md2.css.gz +0 -0
  27. copyparty/web/md2.js.gz +0 -0
  28. copyparty/web/mde.css.gz +0 -0
  29. copyparty/web/mde.html +1 -1
  30. copyparty/web/mde.js.gz +0 -0
  31. copyparty/web/msg.css.gz +0 -0
  32. copyparty/web/msg.html +1 -1
  33. copyparty/web/splash.css.gz +0 -0
  34. copyparty/web/splash.html +4 -2
  35. copyparty/web/splash.js.gz +0 -0
  36. copyparty/web/svcs.html +1 -1
  37. copyparty/web/ui.css.gz +0 -0
  38. copyparty/web/up2k.js.gz +0 -0
  39. copyparty/web/util.js.gz +0 -0
  40. {copyparty-1.10.2.dist-info → copyparty-1.11.1.dist-info}/METADATA +62 -20
  41. {copyparty-1.10.2.dist-info → copyparty-1.11.1.dist-info}/RECORD +45 -43
  42. {copyparty-1.10.2.dist-info → copyparty-1.11.1.dist-info}/WHEEL +1 -1
  43. {copyparty-1.10.2.dist-info → copyparty-1.11.1.dist-info}/LICENSE +0 -0
  44. {copyparty-1.10.2.dist-info → copyparty-1.11.1.dist-info}/entry_points.txt +0 -0
  45. {copyparty-1.10.2.dist-info → copyparty-1.11.1.dist-info}/top_level.txt +0 -0
copyparty/authsrv.py CHANGED
@@ -18,7 +18,6 @@ from .cfg import flagdescs, permdescs, vf_bmap, vf_cmap, vf_vmap
18
18
  from .pwhash import PWHash
19
19
  from .util import (
20
20
  IMPLICATIONS,
21
- META_NOBOTS,
22
21
  SQLITE_VER,
23
22
  UNPLICATIONS,
24
23
  UTC,
@@ -34,6 +33,7 @@ from .util import (
34
33
  uncyg,
35
34
  undot,
36
35
  unhumanize,
36
+ vsplit,
37
37
  )
38
38
 
39
39
  if TYPE_CHECKING:
@@ -54,6 +54,10 @@ BAD_CFG = "invalid config; {}".format(SEE_LOG)
54
54
  SBADCFG = " ({})".format(BAD_CFG)
55
55
 
56
56
 
57
+ class CfgEx(Exception):
58
+ pass
59
+
60
+
57
61
  class AXS(object):
58
62
  def __init__(
59
63
  self,
@@ -773,6 +777,20 @@ class AuthSrv(object):
773
777
  self.line_ctr = 0
774
778
  self.indent = ""
775
779
 
780
+ # fwd-decl
781
+ self.vfs = VFS(log_func, "", "", AXS(), {})
782
+ self.acct = {}
783
+ self.iacct = {}
784
+ self.grps = {}
785
+ self.re_pwd = None
786
+
787
+ # all volumes observed since last restart
788
+ self.idp_vols = {} # vpath->abspath
789
+
790
+ # all users/groups observed since last restart
791
+ self.idp_accs = {} # username->groupnames
792
+ self.idp_usr_gh = {} # username->group-header-value (cache)
793
+
776
794
  self.mutex = threading.Lock()
777
795
  self.reload()
778
796
 
@@ -790,6 +808,86 @@ class AuthSrv(object):
790
808
 
791
809
  yield prev, True
792
810
 
811
+ def idp_checkin(
812
+ self, broker , uname , gname
813
+ ) :
814
+ if uname in self.acct:
815
+ return False
816
+
817
+ if self.idp_usr_gh.get(uname) == gname:
818
+ return False
819
+
820
+ gnames = [x.strip() for x in self.args.idp_gsep.split(gname)]
821
+ gnames.sort()
822
+
823
+ with self.mutex:
824
+ self.idp_usr_gh[uname] = gname
825
+ if self.idp_accs.get(uname) == gnames:
826
+ return False
827
+
828
+ self.idp_accs[uname] = gnames
829
+
830
+ t = "reinitializing due to new user from IdP: [%s:%s]"
831
+ self.log(t % (uname, gnames), 3)
832
+
833
+ if not broker:
834
+ # only true for tests
835
+ self._reload()
836
+ return True
837
+
838
+ broker.ask("_reload_blocking", False).get()
839
+ return True
840
+
841
+ def _map_volume_idp(
842
+ self,
843
+ src ,
844
+ dst ,
845
+ mount ,
846
+ daxs ,
847
+ mflags ,
848
+ un_gns ,
849
+ ) :
850
+ ret = []
851
+ visited = set()
852
+ src0 = src # abspath
853
+ dst0 = dst # vpath
854
+
855
+ un_gn = [(un, gn) for un, gns in un_gns.items() for gn in gns]
856
+ if not un_gn:
857
+ # ensure volume creation if there's no users
858
+ un_gn = [("", "")]
859
+
860
+ for un, gn in un_gn:
861
+ # if ap/vp has a user/group placeholder, make sure to keep
862
+ # track so the same user/gruop is mapped when setting perms;
863
+ # otherwise clear un/gn to indicate it's a regular volume
864
+
865
+ src1 = src0.replace("${u}", un or "\n")
866
+ dst1 = dst0.replace("${u}", un or "\n")
867
+ if src0 == src1 and dst0 == dst1:
868
+ un = ""
869
+
870
+ src = src1.replace("${g}", gn or "\n")
871
+ dst = dst1.replace("${g}", gn or "\n")
872
+ if src == src1 and dst == dst1:
873
+ gn = ""
874
+
875
+ if "\n" in (src + dst):
876
+ continue
877
+
878
+ label = "%s\n%s" % (src, dst)
879
+ if label in visited:
880
+ continue
881
+ visited.add(label)
882
+
883
+ src, dst = self._map_volume(src, dst, mount, daxs, mflags)
884
+ if src:
885
+ ret.append((src, dst, un, gn))
886
+ if un or gn:
887
+ self.idp_vols[dst] = src
888
+
889
+ return ret
890
+
793
891
  def _map_volume(
794
892
  self,
795
893
  src ,
@@ -797,7 +895,11 @@ class AuthSrv(object):
797
895
  mount ,
798
896
  daxs ,
799
897
  mflags ,
800
- ) :
898
+ ) :
899
+ src = os.path.expandvars(os.path.expanduser(src))
900
+ src = absreal(src)
901
+ dst = dst.strip("/")
902
+
801
903
  if dst in mount:
802
904
  t = "multiple filesystem-paths mounted at [/{}]:\n [{}]\n [{}]"
803
905
  self.log(t.format(dst, mount[dst], src), c=1)
@@ -818,6 +920,7 @@ class AuthSrv(object):
818
920
  mount[dst] = src
819
921
  daxs[dst] = AXS()
820
922
  mflags[dst] = {}
923
+ return (src, dst)
821
924
 
822
925
  def _e(self, desc = None) :
823
926
  if not self.args.vc or not self.line_ctr:
@@ -845,31 +948,76 @@ class AuthSrv(object):
845
948
 
846
949
  self.log(t.format(self.line_ctr, c, self.indent, ln, desc))
847
950
 
951
+ def _all_un_gn(
952
+ self,
953
+ acct ,
954
+ grps ,
955
+ ) :
956
+ """
957
+ generate list of all confirmed pairs of username/groupname seen since last restart;
958
+ in case of conflicting group memberships then it is selected as follows:
959
+ * any non-zero value from IdP group header
960
+ * otherwise take --grps / [groups]
961
+ """
962
+ ret = {un: gns[:] for un, gns in self.idp_accs.items()}
963
+ ret.update({zs: [""] for zs in acct if zs not in ret})
964
+ for gn, uns in grps.items():
965
+ for un in uns:
966
+ try:
967
+ ret[un].append(gn)
968
+ except:
969
+ ret[un] = [gn]
970
+
971
+ return ret
972
+
848
973
  def _parse_config_file(
849
974
  self,
850
975
  fp ,
851
976
  cfg_lines ,
852
977
  acct ,
978
+ grps ,
853
979
  daxs ,
854
980
  mflags ,
855
981
  mount ,
856
982
  ) :
857
983
  self.line_ctr = 0
858
984
 
859
- expand_config_file(cfg_lines, fp, "")
985
+ expand_config_file(self.log, cfg_lines, fp, "")
860
986
  if self.args.vc:
861
987
  lns = ["{:4}: {}".format(n, s) for n, s in enumerate(cfg_lines, 1)]
862
988
  self.log("expanded config file (unprocessed):\n" + "\n".join(lns))
863
989
 
864
990
  cfg_lines = upgrade_cfg_fmt(self.log, self.args, cfg_lines, fp)
865
991
 
992
+ # due to IdP, volumes must be parsed after users and groups;
993
+ # do volumes in a 2nd pass to allow arbitrary order in config files
994
+ for npass in range(1, 3):
995
+ if self.args.vc:
996
+ self.log("parsing config files; pass %d/%d" % (npass, 2))
997
+ self._parse_config_file_2(cfg_lines, acct, grps, daxs, mflags, mount, npass)
998
+
999
+ def _parse_config_file_2(
1000
+ self,
1001
+ cfg_lines ,
1002
+ acct ,
1003
+ grps ,
1004
+ daxs ,
1005
+ mflags ,
1006
+ mount ,
1007
+ npass ,
1008
+ ) :
1009
+ self.line_ctr = 0
1010
+ all_un_gn = self._all_un_gn(acct, grps)
1011
+
866
1012
  cat = ""
867
1013
  catg = "[global]"
868
1014
  cata = "[accounts]"
1015
+ catgrp = "[groups]"
869
1016
  catx = "accs:"
870
1017
  catf = "flags:"
871
1018
  ap = None
872
1019
  vp = None
1020
+ vols = []
873
1021
  for ln in cfg_lines:
874
1022
  self.line_ctr += 1
875
1023
  ln = ln.split(" #")[0].strip()
@@ -882,7 +1030,7 @@ class AuthSrv(object):
882
1030
  subsection = ln in (catx, catf)
883
1031
  if ln.startswith("[") or subsection:
884
1032
  self._e()
885
- if ap is None and vp is not None:
1033
+ if npass > 1 and ap is None and vp is not None:
886
1034
  t = "the first line after [/{}] must be a filesystem path to share on that volume"
887
1035
  raise Exception(t.format(vp))
888
1036
 
@@ -898,6 +1046,8 @@ class AuthSrv(object):
898
1046
  self._l(ln, 6, t)
899
1047
  elif ln == cata:
900
1048
  self._l(ln, 5, "begin user-accounts section")
1049
+ elif ln == catgrp:
1050
+ self._l(ln, 5, "begin user-groups section")
901
1051
  elif ln.startswith("[/"):
902
1052
  vp = ln[1:-1].strip("/")
903
1053
  self._l(ln, 2, "define volume at URL [/{}]".format(vp))
@@ -934,15 +1084,39 @@ class AuthSrv(object):
934
1084
  raise Exception(t + SBADCFG)
935
1085
  continue
936
1086
 
1087
+ if cat == catgrp:
1088
+ try:
1089
+ gn, zs1 = [zs.strip() for zs in ln.split(":", 1)]
1090
+ uns = [zs.strip() for zs in zs1.split(",")]
1091
+ t = "group [%s] = " % (gn,)
1092
+ t += ", ".join("user [%s]" % (x,) for x in uns)
1093
+ self._l(ln, 5, t)
1094
+ grps[gn] = uns
1095
+ except:
1096
+ t = 'lines inside the [groups] section must be "groupname: user1, user2, user..."'
1097
+ raise Exception(t + SBADCFG)
1098
+ continue
1099
+
937
1100
  if vp is not None and ap is None:
1101
+ if npass != 2:
1102
+ continue
1103
+
938
1104
  ap = ln
939
- ap = os.path.expandvars(os.path.expanduser(ap))
940
- ap = absreal(ap)
941
1105
  self._l(ln, 2, "bound to filesystem-path [{}]".format(ap))
942
- self._map_volume(ap, vp, mount, daxs, mflags)
1106
+ vols = self._map_volume_idp(ap, vp, mount, daxs, mflags, all_un_gn)
1107
+ if not vols:
1108
+ ap = vp = None
1109
+ self._l(ln, 2, "└─no users/groups known; was not mapped")
1110
+ elif len(vols) > 1:
1111
+ for vol in vols:
1112
+ self._l(ln, 2, "└─mapping: [%s] => [%s]" % (vol[1], vol[0]))
943
1113
  continue
944
1114
 
945
1115
  if cat == catx:
1116
+ if npass != 2 or not ap:
1117
+ # not stage2, or unmapped ${u}/${g}
1118
+ continue
1119
+
946
1120
  err = ""
947
1121
  try:
948
1122
  self._l(ln, 5, "volume access config:")
@@ -953,14 +1127,20 @@ class AuthSrv(object):
953
1127
  if " " in re.sub(", *", "", sv).strip():
954
1128
  err = "list of users is not comma-separated; "
955
1129
  raise Exception(err)
956
- assert vp is not None
957
- self._read_vol_str(sk, sv.replace(" ", ""), daxs[vp], mflags[vp])
1130
+ sv = sv.replace(" ", "")
1131
+ self._read_vol_str_idp(sk, sv, vols, all_un_gn, daxs, mflags)
958
1132
  continue
1133
+ except CfgEx:
1134
+ raise
959
1135
  except:
960
1136
  err += "accs entries must be 'rwmdgGhaA.: user1, user2, ...'"
961
- raise Exception(err + SBADCFG)
1137
+ raise CfgEx(err + SBADCFG)
962
1138
 
963
1139
  if cat == catf:
1140
+ if npass != 2 or not ap:
1141
+ # not stage2, or unmapped ${u}/${g}
1142
+ continue
1143
+
964
1144
  err = ""
965
1145
  try:
966
1146
  self._l(ln, 6, "volume-specific config:")
@@ -977,11 +1157,14 @@ class AuthSrv(object):
977
1157
  else:
978
1158
  fstr += ",{}={}".format(sk, sv)
979
1159
  assert vp is not None
980
- self._read_vol_str("c", fstr[1:], daxs[vp], mflags[vp])
1160
+ self._read_vol_str_idp(
1161
+ "c", fstr[1:], vols, all_un_gn, daxs, mflags
1162
+ )
981
1163
  fstr = ""
982
1164
  if fstr:
983
- assert vp is not None
984
- self._read_vol_str("c", fstr[1:], daxs[vp], mflags[vp])
1165
+ self._read_vol_str_idp(
1166
+ "c", fstr[1:], vols, all_un_gn, daxs, mflags
1167
+ )
985
1168
  continue
986
1169
  except:
987
1170
  err += "flags entries (volflags) must be one of the following:\n 'flag1, flag2, ...'\n 'key: value'\n 'flag1, flag2, key: value'"
@@ -992,12 +1175,18 @@ class AuthSrv(object):
992
1175
  self._e()
993
1176
  self.line_ctr = 0
994
1177
 
995
- def _read_vol_str(
996
- self, lvl , uname , axs , flags
1178
+ def _read_vol_str_idp(
1179
+ self,
1180
+ lvl ,
1181
+ uname ,
1182
+ vols ,
1183
+ un_gns ,
1184
+ axs ,
1185
+ flags ,
997
1186
  ) :
998
1187
  if lvl.strip("crwmdgGhaA."):
999
1188
  t = "%s,%s" % (lvl, uname) if uname else lvl
1000
- raise Exception("invalid config value (volume or volflag): %s" % (t,))
1189
+ raise CfgEx("invalid config value (volume or volflag): %s" % (t,))
1001
1190
 
1002
1191
  if lvl == "c":
1003
1192
  # here, 'uname' is not a username; it is a volflag name... sorry
@@ -1012,16 +1201,63 @@ class AuthSrv(object):
1012
1201
  while "," in uname:
1013
1202
  # one or more bools before the final flag; eat them
1014
1203
  n1, uname = uname.split(",", 1)
1015
- self._read_volflag(flags, n1, True, False)
1204
+ for _, vp, _, _ in vols:
1205
+ self._read_volflag(flags[vp], n1, True, False)
1206
+
1207
+ for _, vp, _, _ in vols:
1208
+ self._read_volflag(flags[vp], uname, cval, False)
1016
1209
 
1017
- self._read_volflag(flags, uname, cval, False)
1018
1210
  return
1019
1211
 
1020
1212
  if uname == "":
1021
1213
  uname = "*"
1022
1214
 
1023
- junkset = set()
1215
+ unames = []
1024
1216
  for un in uname.replace(",", " ").strip().split():
1217
+ if un.startswith("@"):
1218
+ grp = un[1:]
1219
+ uns = [x[0] for x in un_gns.items() if grp in x[1]]
1220
+ if not uns and grp != "${g}" and not self.args.idp_h_grp:
1221
+ t = "group [%s] must be defined with --grp argument (or in a [groups] config section)"
1222
+ raise CfgEx(t % (grp,))
1223
+
1224
+ unames.extend(uns)
1225
+ else:
1226
+ unames.append(un)
1227
+
1228
+ # unames may still contain ${u} and ${g} so now expand those;
1229
+ un_gn = [(un, gn) for un, gns in un_gns.items() for gn in gns]
1230
+ if "*" not in un_gns:
1231
+ # need ("*","") to match "*" in unames
1232
+ un_gn.append(("*", ""))
1233
+
1234
+ for _, dst, vu, vg in vols:
1235
+ unames2 = set()
1236
+ for un, gn in un_gn:
1237
+ # if vu/vg (volume user/group) is non-null,
1238
+ # then each non-null value corresponds to
1239
+ # ${u}/${g}; consider this a filter to
1240
+ # apply to unames, as well as un_gn
1241
+ if (vu and vu != un) or (vg and vg != gn):
1242
+ continue
1243
+
1244
+ for uname in unames + ([un] if vu or vg else []):
1245
+ if uname == "${u}":
1246
+ uname = vu or un
1247
+ elif uname in ("${g}", "@${g}"):
1248
+ uname = vg or gn
1249
+
1250
+ if vu and vu != uname:
1251
+ continue
1252
+
1253
+ if uname:
1254
+ unames2.add(uname)
1255
+
1256
+ self._read_vol_str(lvl, list(unames2), axs[dst])
1257
+
1258
+ def _read_vol_str(self, lvl , unames , axs ) :
1259
+ junkset = set()
1260
+ for un in unames:
1025
1261
  for alias, mapping in [
1026
1262
  ("h", "gh"),
1027
1263
  ("G", "gG"),
@@ -1098,12 +1334,18 @@ class AuthSrv(object):
1098
1334
  then supplementing with config files
1099
1335
  before finally building the VFS
1100
1336
  """
1337
+ with self.mutex:
1338
+ self._reload()
1101
1339
 
1340
+ def _reload(self) :
1102
1341
  acct = {} # username:password
1342
+ grps = {} # groupname:usernames
1103
1343
  daxs = {}
1104
1344
  mflags = {} # moutpoint:flags
1105
1345
  mount = {} # dst:src (mountpoint:realpath)
1106
1346
 
1347
+ self.idp_vols = {} # yolo
1348
+
1107
1349
  if self.args.a:
1108
1350
  # list of username:password
1109
1351
  for x in self.args.a:
@@ -1114,9 +1356,22 @@ class AuthSrv(object):
1114
1356
  t = '\n invalid value "{}" for argument -a, must be username:password'
1115
1357
  raise Exception(t.format(x))
1116
1358
 
1359
+ if self.args.grp:
1360
+ # list of groupname:username,username,...
1361
+ for x in self.args.grp:
1362
+ try:
1363
+ # accept both = and : as separator between groupname and usernames,
1364
+ # accept both , and : as separators between usernames
1365
+ zs1, zs2 = x.replace("=", ":").split(":", 1)
1366
+ grps[zs1] = zs2.replace(":", ",").split(",")
1367
+ except:
1368
+ t = '\n invalid value "{}" for argument --grp, must be groupname:username1,username2,...'
1369
+ raise Exception(t.format(x))
1370
+
1117
1371
  if self.args.v:
1118
1372
  # list of src:dst:permset:permset:...
1119
1373
  # permset is <rwmdgGhaA.>[,username][,username] or <c>,<flag>[=args]
1374
+ all_un_gn = self._all_un_gn(acct, grps)
1120
1375
  for v_str in self.args.v:
1121
1376
  m = re_vol.match(v_str)
1122
1377
  if not m:
@@ -1126,20 +1381,19 @@ class AuthSrv(object):
1126
1381
  if WINDOWS:
1127
1382
  src = uncyg(src)
1128
1383
 
1129
- # print("\n".join([src, dst, perms]))
1130
- src = absreal(src)
1131
- dst = dst.strip("/")
1132
- self._map_volume(src, dst, mount, daxs, mflags)
1384
+ vols = self._map_volume_idp(src, dst, mount, daxs, mflags, all_un_gn)
1133
1385
 
1134
1386
  for x in perms.split(":"):
1135
1387
  lvl, uname = x.split(",", 1) if "," in x else [x, ""]
1136
- self._read_vol_str(lvl, uname, daxs[dst], mflags[dst])
1388
+ self._read_vol_str_idp(lvl, uname, vols, all_un_gn, daxs, mflags)
1137
1389
 
1138
1390
  if self.args.c:
1139
1391
  for cfg_fn in self.args.c:
1140
1392
  lns = []
1141
1393
  try:
1142
- self._parse_config_file(cfg_fn, lns, acct, daxs, mflags, mount)
1394
+ self._parse_config_file(
1395
+ cfg_fn, lns, acct, grps, daxs, mflags, mount
1396
+ )
1143
1397
 
1144
1398
  zs = "#\033[36m cfg files in "
1145
1399
  zst = [x[len(zs) :] for x in lns if x.startswith(zs)]
@@ -1170,7 +1424,7 @@ class AuthSrv(object):
1170
1424
 
1171
1425
  mount = cased
1172
1426
 
1173
- if not mount:
1427
+ if not mount and not self.args.idp_h_usr:
1174
1428
  # -h says our defaults are CWD at root and read/write for everyone
1175
1429
  axs = AXS(["*"], ["*"], None, None)
1176
1430
  vfs = VFS(self.log_func, absreal("."), "", axs, {})
@@ -1206,9 +1460,13 @@ class AuthSrv(object):
1206
1460
  vol.all_vps.sort(key=lambda x: len(x[0]), reverse=True)
1207
1461
  vol.root = vfs
1208
1462
 
1463
+ zss = set(acct)
1464
+ zss.update(self.idp_accs)
1465
+ zss.discard("*")
1466
+ unames = ["*"] + list(sorted(zss))
1467
+
1209
1468
  for perm in "read write move del get pget html admin dot".split():
1210
1469
  axs_key = "u" + perm
1211
- unames = ["*"] + list(acct.keys())
1212
1470
  for vp, vol in vfs.all_vols.items():
1213
1471
  zx = getattr(vol.axs, axs_key)
1214
1472
  if "*" in zx:
@@ -1242,18 +1500,20 @@ class AuthSrv(object):
1242
1500
  ]:
1243
1501
  for usr in d:
1244
1502
  all_users[usr] = 1
1245
- if usr != "*" and usr not in acct:
1503
+ if usr != "*" and usr not in acct and usr not in self.idp_accs:
1246
1504
  missing_users[usr] = 1
1247
1505
  if "*" not in d:
1248
1506
  associated_users[usr] = 1
1249
1507
 
1250
1508
  if missing_users:
1251
- self.log(
1252
- "you must -a the following users: "
1253
- + ", ".join(k for k in sorted(missing_users)),
1254
- c=1,
1255
- )
1256
- raise Exception(BAD_CFG)
1509
+ zs = ", ".join(k for k in sorted(missing_users))
1510
+ if self.args.idp_h_usr:
1511
+ t = "the following users are unknown, and assumed to come from IdP: "
1512
+ self.log(t + zs, c=6)
1513
+ else:
1514
+ t = "you must -a the following users: "
1515
+ self.log(t + zs, c=1)
1516
+ raise Exception(BAD_CFG)
1257
1517
 
1258
1518
  if LEELOO_DALLAS in all_users:
1259
1519
  raise Exception("sorry, reserved username: " + LEELOO_DALLAS)
@@ -1394,13 +1654,6 @@ class AuthSrv(object):
1394
1654
  if not vol.flags.get("robots"):
1395
1655
  vol.flags["norobots"] = True
1396
1656
 
1397
- for vol in vfs.all_vols.values():
1398
- h = [vol.flags.get("html_head", self.args.html_head)]
1399
- if vol.flags.get("norobots"):
1400
- h.insert(0, META_NOBOTS)
1401
-
1402
- vol.flags["html_head"] = "\n".join([x for x in h if x])
1403
-
1404
1657
  for vol in vfs.all_vols.values():
1405
1658
  if self.args.no_vthumb:
1406
1659
  vol.flags["dvthumb"] = True
@@ -1478,7 +1731,7 @@ class AuthSrv(object):
1478
1731
  if k not in vol.flags:
1479
1732
  vol.flags[k] = getattr(self.args, k)
1480
1733
 
1481
- for k in ("nrand",):
1734
+ for k in ("nrand", "u2abort"):
1482
1735
  if k in vol.flags:
1483
1736
  vol.flags[k] = int(vol.flags[k])
1484
1737
 
@@ -1742,20 +1995,37 @@ class AuthSrv(object):
1742
1995
  except Pebkac:
1743
1996
  self.warn_anonwrite = True
1744
1997
 
1745
- with self.mutex:
1746
- self.vfs = vfs
1747
- self.acct = acct
1748
- self.iacct = {v: k for k, v in acct.items()}
1749
-
1750
- self.re_pwd = None
1751
- pwds = [re.escape(x) for x in self.iacct.keys()]
1752
- if pwds:
1753
- if self.ah.on:
1754
- zs = r"(\[H\] pw:.*|[?&]pw=)([^&]+)"
1755
- else:
1756
- zs = r"(\[H\] pw:.*|=)(" + "|".join(pwds) + r")([]&; ]|$)"
1998
+ idp_err = "WARNING! The following IdP volumes are mounted directly below another volume where anonymous users can read and/or write files. This is a SECURITY HAZARD!! When copyparty is restarted, it will not know about these IdP volumes yet. These volumes will then be accessible by anonymous users UNTIL one of the users associated with their volume sends a request to the server. RECOMMENDATION: You should create a restricted volume where nobody can read/write files, and make sure that all IdP volumes are configured to appear somewhere below that volume."
1999
+ for idp_vp in self.idp_vols:
2000
+ parent_vp = vsplit(idp_vp)[0]
2001
+ vn, _ = vfs.get(parent_vp, "*", False, False)
2002
+ zs = (
2003
+ "READABLE"
2004
+ if "*" in vn.axs.uread
2005
+ else "WRITABLE"
2006
+ if "*" in vn.axs.uwrite
2007
+ else ""
2008
+ )
2009
+ if zs:
2010
+ t = '\nWARNING: Volume "/%s" appears below "/%s" and would be WORLD-%s'
2011
+ idp_err += t % (idp_vp, vn.vpath, zs)
2012
+ if "\n" in idp_err:
2013
+ self.log(idp_err, 1)
2014
+
2015
+ self.vfs = vfs
2016
+ self.acct = acct
2017
+ self.grps = grps
2018
+ self.iacct = {v: k for k, v in acct.items()}
2019
+
2020
+ self.re_pwd = None
2021
+ pwds = [re.escape(x) for x in self.iacct.keys()]
2022
+ if pwds:
2023
+ if self.ah.on:
2024
+ zs = r"(\[H\] pw:.*|[?&]pw=)([^&]+)"
2025
+ else:
2026
+ zs = r"(\[H\] pw:.*|=)(" + "|".join(pwds) + r")([]&; ]|$)"
1757
2027
 
1758
- self.re_pwd = re.compile(zs)
2028
+ self.re_pwd = re.compile(zs)
1759
2029
 
1760
2030
  def setup_pwhash(self, acct ) :
1761
2031
  self.ah = PWHash(self.args)
@@ -1997,6 +2267,12 @@ class AuthSrv(object):
1997
2267
  ret.append(" {}: {}".format(u, p))
1998
2268
  ret.append("")
1999
2269
 
2270
+ if self.grps:
2271
+ ret.append("[groups]")
2272
+ for gn, uns in self.grps.items():
2273
+ ret.append(" %s: %s" % (gn, ", ".join(uns)))
2274
+ ret.append("")
2275
+
2000
2276
  for vol in self.vfs.all_vols.values():
2001
2277
  ret.append("[/{}]".format(vol.vpath))
2002
2278
  ret.append(" " + vol.realpath)
@@ -2094,27 +2370,50 @@ def split_cfg_ln(ln ) :
2094
2370
  return ret
2095
2371
 
2096
2372
 
2097
- def expand_config_file(ret , fp , ipath ) :
2373
+ def expand_config_file(
2374
+ log , ret , fp , ipath
2375
+ ) :
2098
2376
  """expand all % file includes"""
2099
2377
  fp = absreal(fp)
2100
2378
  if len(ipath.split(" -> ")) > 64:
2101
2379
  raise Exception("hit max depth of 64 includes")
2102
2380
 
2103
2381
  if os.path.isdir(fp):
2104
- names = os.listdir(fp)
2105
- crumb = "#\033[36m cfg files in {} => {}\033[0m".format(fp, names)
2106
- ret.append(crumb)
2107
- for fn in sorted(names):
2382
+ names = list(sorted(os.listdir(fp)))
2383
+ cnames = [x for x in names if x.lower().endswith(".conf")]
2384
+ if not cnames:
2385
+ t = "warning: tried to read config-files from folder '%s' but it does not contain any "
2386
+ if names:
2387
+ t += ".conf files; the following files were ignored: %s"
2388
+ t = t % (fp, ", ".join(names[:8]))
2389
+ else:
2390
+ t += "files at all"
2391
+ t = t % (fp,)
2392
+
2393
+ if log:
2394
+ log(t, 3)
2395
+
2396
+ ret.append("#\033[33m %s\033[0m" % (t,))
2397
+ else:
2398
+ zs = "#\033[36m cfg files in %s => %s\033[0m" % (fp, cnames)
2399
+ ret.append(zs)
2400
+
2401
+ for fn in cnames:
2108
2402
  fp2 = os.path.join(fp, fn)
2109
- if not fp2.endswith(".conf") or fp2 in ipath:
2403
+ if fp2 in ipath:
2110
2404
  continue
2111
2405
 
2112
- expand_config_file(ret, fp2, ipath)
2406
+ expand_config_file(log, ret, fp2, ipath)
2407
+
2408
+ return
2113
2409
 
2114
- if ret[-1] == crumb:
2115
- # no config files below; remove breadcrumb
2116
- ret.pop()
2410
+ if not os.path.exists(fp):
2411
+ t = "warning: tried to read config from '%s' but the file/folder does not exist"
2412
+ t = t % (fp,)
2413
+ if log:
2414
+ log(t, 3)
2117
2415
 
2416
+ ret.append("#\033[31m %s\033[0m" % (t,))
2118
2417
  return
2119
2418
 
2120
2419
  ipath += " -> " + fp
@@ -2128,7 +2427,7 @@ def expand_config_file(ret , fp , ipath ) :
2128
2427
  fp2 = ln[1:].strip()
2129
2428
  fp2 = os.path.join(os.path.dirname(fp), fp2)
2130
2429
  ofs = len(ret)
2131
- expand_config_file(ret, fp2, ipath)
2430
+ expand_config_file(log, ret, fp2, ipath)
2132
2431
  for n in range(ofs, len(ret)):
2133
2432
  ret[n] = pad + ret[n]
2134
2433
  continue