pyftpd-sink 4.3 KB

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