pyftpd_sink.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. import argparse
  2. import hashlib
  3. import logging
  4. import os
  5. import pyftpdlib.authorizers
  6. import pyftpdlib.handlers
  7. import pyftpdlib.servers
  8. LOG_LEVELS = {
  9. 'critical': logging.CRITICAL,
  10. 'error': logging.ERROR,
  11. 'warning': logging.WARNING,
  12. 'info': logging.INFO,
  13. 'debug': logging.DEBUG,
  14. }
  15. class SHA256Authorizer(pyftpdlib.authorizers.DummyAuthorizer):
  16. def validate_authentication(self, username, password, handler):
  17. # pyftpdlib/authorizers.py", line 110, in add_user
  18. # dic = {'pwd': str(password),
  19. # so we can not compare against .digest()
  20. password_hash = hashlib.sha256(password.encode()).hexdigest()
  21. if self.user_table[username]['pwd'] != password_hash.lower():
  22. raise pyftpdlib.authorizers.AuthenticationFailed()
  23. def serve(root_dir_path, username, password_sha256_hexdigest, control_port, passive_port, passive_address, log_level):
  24. logging.basicConfig(level=log_level)
  25. assert os.path.isdir(root_dir_path), root_dir_path
  26. authorizer = SHA256Authorizer()
  27. authorizer.add_user(
  28. username,
  29. password_sha256_hexdigest.lower(),
  30. homedir=root_dir_path,
  31. # https://pyftpdlib.readthedocs.io/en/latest/api.html#pyftpdlib.authorizers.DummyAuthorizer.add_user
  32. # e: change dir
  33. # m: mkdir
  34. # w: write
  35. perm='emw',
  36. msg_login='renshi ni hen gaoxing',
  37. msg_quit='zaijian',
  38. )
  39. handler = pyftpdlib.handlers.FTPHandler
  40. handler.authorizer = authorizer
  41. handler.banner = 'ni hao'
  42. handler.passive_ports = (passive_port,)
  43. handler.masquerade_address = passive_address
  44. server = pyftpdlib.servers.FTPServer((None, control_port), handler)
  45. # apparently requires +1 for unknown reasons
  46. server.max_cons = 1 + 1
  47. server.serve_forever()
  48. class EnvDefaultArgparser(argparse.ArgumentParser):
  49. def add_argument(self, *args, envvar=None, **kwargs):
  50. if envvar:
  51. envvar_value = os.environ.get(envvar, None)
  52. if envvar_value:
  53. kwargs['required'] = False
  54. kwargs['default'] = envvar_value
  55. super().add_argument(*args, **kwargs)
  56. def _init_argparser():
  57. argparser = EnvDefaultArgparser()
  58. argparser.add_argument(
  59. '--root', '--root-dir',
  60. metavar='path',
  61. dest='root_dir_path',
  62. default=os.getcwd(),
  63. help='default: current working directory',
  64. )
  65. argparser.add_argument(
  66. '--user', '--username',
  67. metavar='username',
  68. dest='username',
  69. required=True,
  70. envvar='FTP_USERNAME',
  71. help='default: env var $FTP_USERNAME',
  72. )
  73. argparser.add_argument(
  74. '--pwd-hash', '--password-hash',
  75. metavar='sha256_hexdigest',
  76. dest='password_sha256_hexdigest',
  77. required=True,
  78. envvar='FTP_PASSWORD_SHA256',
  79. help='default: env var $FTP_PASSWORD_SHA256',
  80. )
  81. argparser.add_argument(
  82. '--ctrl-port', '--control-port',
  83. metavar='port',
  84. type=int,
  85. dest='control_port',
  86. envvar='FTP_CONTROL_PORT',
  87. default=2121,
  88. help='default: env var $FTP_CONTROL_PORT or 2121',
  89. )
  90. argparser.add_argument(
  91. '--pasv-port', '--passive-port',
  92. metavar='port',
  93. type=int,
  94. dest='passive_port',
  95. envvar='FTP_PASSIVE_PORT',
  96. default=62121,
  97. help='port for passive (PASV) & extended passive (EPSV) mode;'
  98. + ' default: env var $FTP_PASSIVE_PORT or 62121',
  99. )
  100. argparser.add_argument(
  101. '--pasv-addr', '--passive-address', '--masquerade-address',
  102. metavar='ip_address',
  103. dest='passive_address',
  104. envvar='FTP_PASSIVE_ADDRESS',
  105. default=None,
  106. help='address returned to client (227)'
  107. + ' after opening port for passive mode (PASV)'
  108. + ' default: socket\'s own address',
  109. )
  110. argparser.add_argument(
  111. '--log-level',
  112. metavar='level_name',
  113. dest='log_level_name',
  114. default='info',
  115. choices=LOG_LEVELS.keys(),
  116. help='default: %(default)s',
  117. )
  118. return argparser
  119. def main():
  120. args = _init_argparser().parse_args()
  121. args.log_level = LOG_LEVELS[args.log_level_name]
  122. del args.log_level_name
  123. serve(**vars(args))