smbserver.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. #
  4. # Project ___| | | | _ \| |
  5. # / __| | | | |_) | |
  6. # | (__| |_| | _ <| |___
  7. # \___|\___/|_| \_\_____|
  8. #
  9. # Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
  10. #
  11. # This software is licensed as described in the file COPYING, which
  12. # you should have received as part of this distribution. The terms
  13. # are also available at https://curl.se/docs/copyright.html.
  14. #
  15. # You may opt to use, copy, modify, merge, publish, distribute and/or sell
  16. # copies of the Software, and permit persons to whom the Software is
  17. # furnished to do so, under the terms of the COPYING file.
  18. #
  19. # This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
  20. # KIND, either express or implied.
  21. #
  22. # SPDX-License-Identifier: curl
  23. #
  24. """Server for testing SMB"""
  25. from __future__ import (absolute_import, division, print_function,
  26. unicode_literals)
  27. import argparse
  28. import logging
  29. import os
  30. import sys
  31. import tempfile
  32. # Import our curl test data helper
  33. from util import ClosingFileHandler, TestData
  34. if sys.version_info.major >= 3:
  35. import configparser
  36. else:
  37. import ConfigParser as configparser
  38. # impacket needs to be installed in the Python environment
  39. try:
  40. import impacket
  41. except ImportError:
  42. sys.stderr.write('Python package impacket needs to be installed!\n')
  43. sys.stderr.write('Use pip or your package manager to install it.\n')
  44. sys.exit(1)
  45. from impacket import smb as imp_smb
  46. from impacket import smbserver as imp_smbserver
  47. from impacket.nt_errors import (STATUS_ACCESS_DENIED, STATUS_NO_SUCH_FILE,
  48. STATUS_SUCCESS)
  49. log = logging.getLogger(__name__)
  50. SERVER_MAGIC = "SERVER_MAGIC"
  51. TESTS_MAGIC = "TESTS_MAGIC"
  52. VERIFIED_REQ = "verifiedserver"
  53. VERIFIED_RSP = "WE ROOLZ: {pid}\n"
  54. def smbserver(options):
  55. """Start up a TCP SMB server that serves forever
  56. """
  57. if options.pidfile:
  58. pid = os.getpid()
  59. # see tests/server/util.c function write_pidfile
  60. if os.name == "nt":
  61. pid += 65536
  62. with open(options.pidfile, "w") as f:
  63. f.write(str(pid))
  64. # Here we write a mini config for the server
  65. smb_config = configparser.ConfigParser()
  66. smb_config.add_section("global")
  67. smb_config.set("global", "server_name", "SERVICE")
  68. smb_config.set("global", "server_os", "UNIX")
  69. smb_config.set("global", "server_domain", "WORKGROUP")
  70. smb_config.set("global", "log_file", "")
  71. smb_config.set("global", "credentials_file", "")
  72. # We need a share which allows us to test that the server is running
  73. smb_config.add_section("SERVER")
  74. smb_config.set("SERVER", "comment", "server function")
  75. smb_config.set("SERVER", "read only", "yes")
  76. smb_config.set("SERVER", "share type", "0")
  77. smb_config.set("SERVER", "path", SERVER_MAGIC)
  78. # Have a share for tests. These files will be autogenerated from the
  79. # test input.
  80. smb_config.add_section("TESTS")
  81. smb_config.set("TESTS", "comment", "tests")
  82. smb_config.set("TESTS", "read only", "yes")
  83. smb_config.set("TESTS", "share type", "0")
  84. smb_config.set("TESTS", "path", TESTS_MAGIC)
  85. if not options.srcdir or not os.path.isdir(options.srcdir):
  86. raise ScriptException("--srcdir is mandatory")
  87. test_data_dir = os.path.join(options.srcdir, "data")
  88. smb_server = TestSmbServer((options.host, options.port),
  89. config_parser=smb_config,
  90. test_data_directory=test_data_dir)
  91. log.info("[SMB] setting up SMB server on port %s", options.port)
  92. smb_server.processConfigFile()
  93. smb_server.serve_forever()
  94. return 0
  95. class TestSmbServer(imp_smbserver.SMBSERVER):
  96. """
  97. Test server for SMB which subclasses the impacket SMBSERVER and provides
  98. test functionality.
  99. """
  100. def __init__(self,
  101. address,
  102. config_parser=None,
  103. test_data_directory=None):
  104. imp_smbserver.SMBSERVER.__init__(self,
  105. address,
  106. config_parser=config_parser)
  107. # Set up a test data object so we can get test data later.
  108. self.ctd = TestData(test_data_directory)
  109. # Override smbComNtCreateAndX so we can pretend to have files which
  110. # don't exist.
  111. self.hookSmbCommand(imp_smb.SMB.SMB_COM_NT_CREATE_ANDX,
  112. self.create_and_x)
  113. def create_and_x(self, conn_id, smb_server, smb_command, recv_packet):
  114. """
  115. Our version of smbComNtCreateAndX looks for special test files and
  116. fools the rest of the framework into opening them as if they were
  117. normal files.
  118. """
  119. conn_data = smb_server.getConnectionData(conn_id)
  120. # Wrap processing in a try block which allows us to throw SmbException
  121. # to control the flow.
  122. try:
  123. ncax_parms = imp_smb.SMBNtCreateAndX_Parameters(
  124. smb_command["Parameters"])
  125. path = self.get_share_path(conn_data,
  126. ncax_parms["RootFid"],
  127. recv_packet["Tid"])
  128. log.info("[SMB] Requested share path: %s", path)
  129. disposition = ncax_parms["Disposition"]
  130. log.debug("[SMB] Requested disposition: %s", disposition)
  131. # Currently we only support reading files.
  132. if disposition != imp_smb.FILE_OPEN:
  133. raise SmbException(STATUS_ACCESS_DENIED,
  134. "Only support reading files")
  135. # Check to see if the path we were given is actually a
  136. # magic path which needs generating on the fly.
  137. if path not in [SERVER_MAGIC, TESTS_MAGIC]:
  138. # Pass the command onto the original handler.
  139. return imp_smbserver.SMBCommands.smbComNtCreateAndX(conn_id,
  140. smb_server,
  141. smb_command,
  142. recv_packet)
  143. flags2 = recv_packet["Flags2"]
  144. ncax_data = imp_smb.SMBNtCreateAndX_Data(flags=flags2,
  145. data=smb_command[
  146. "Data"])
  147. requested_file = imp_smbserver.decodeSMBString(
  148. flags2,
  149. ncax_data["FileName"])
  150. log.debug("[SMB] User requested file '%s'", requested_file)
  151. if path == SERVER_MAGIC:
  152. fid, full_path = self.get_server_path(requested_file)
  153. else:
  154. assert (path == TESTS_MAGIC)
  155. fid, full_path = self.get_test_path(requested_file)
  156. resp_parms = imp_smb.SMBNtCreateAndXResponse_Parameters()
  157. resp_data = ""
  158. # Simple way to generate a fid
  159. if len(conn_data["OpenedFiles"]) == 0:
  160. fakefid = 1
  161. else:
  162. fakefid = conn_data["OpenedFiles"].keys()[-1] + 1
  163. resp_parms["Fid"] = fakefid
  164. resp_parms["CreateAction"] = disposition
  165. if os.path.isdir(path):
  166. resp_parms[
  167. "FileAttributes"] = imp_smb.SMB_FILE_ATTRIBUTE_DIRECTORY
  168. resp_parms["IsDirectory"] = 1
  169. else:
  170. resp_parms["IsDirectory"] = 0
  171. resp_parms["FileAttributes"] = ncax_parms["FileAttributes"]
  172. # Get this file's information
  173. resp_info, error_code = imp_smbserver.queryPathInformation(
  174. os.path.dirname(full_path), os.path.basename(full_path),
  175. level=imp_smb.SMB_QUERY_FILE_ALL_INFO)
  176. if error_code != STATUS_SUCCESS:
  177. raise SmbException(error_code, "Failed to query path info")
  178. resp_parms["CreateTime"] = resp_info["CreationTime"]
  179. resp_parms["LastAccessTime"] = resp_info[
  180. "LastAccessTime"]
  181. resp_parms["LastWriteTime"] = resp_info["LastWriteTime"]
  182. resp_parms["LastChangeTime"] = resp_info[
  183. "LastChangeTime"]
  184. resp_parms["FileAttributes"] = resp_info[
  185. "ExtFileAttributes"]
  186. resp_parms["AllocationSize"] = resp_info[
  187. "AllocationSize"]
  188. resp_parms["EndOfFile"] = resp_info["EndOfFile"]
  189. # Let's store the fid for the connection
  190. # smbServer.log("Create file %s, mode:0x%x" % (pathName, mode))
  191. conn_data["OpenedFiles"][fakefid] = {}
  192. conn_data["OpenedFiles"][fakefid]["FileHandle"] = fid
  193. conn_data["OpenedFiles"][fakefid]["FileName"] = path
  194. conn_data["OpenedFiles"][fakefid]["DeleteOnClose"] = False
  195. except SmbException as s:
  196. log.debug("[SMB] SmbException hit: %s", s)
  197. error_code = s.error_code
  198. resp_parms = ""
  199. resp_data = ""
  200. resp_cmd = imp_smb.SMBCommand(imp_smb.SMB.SMB_COM_NT_CREATE_ANDX)
  201. resp_cmd["Parameters"] = resp_parms
  202. resp_cmd["Data"] = resp_data
  203. smb_server.setConnectionData(conn_id, conn_data)
  204. return [resp_cmd], None, error_code
  205. def get_share_path(self, conn_data, root_fid, tid):
  206. conn_shares = conn_data["ConnectedShares"]
  207. if tid in conn_shares:
  208. if root_fid > 0:
  209. # If we have a rootFid, the path is relative to that fid
  210. path = conn_data["OpenedFiles"][root_fid]["FileName"]
  211. log.debug("RootFid present %s!" % path)
  212. else:
  213. if "path" in conn_shares[tid]:
  214. path = conn_shares[tid]["path"]
  215. else:
  216. raise SmbException(STATUS_ACCESS_DENIED,
  217. "Connection share had no path")
  218. else:
  219. raise SmbException(imp_smbserver.STATUS_SMB_BAD_TID,
  220. "TID was invalid")
  221. return path
  222. def get_server_path(self, requested_filename):
  223. log.debug("[SMB] Get server path '%s'", requested_filename)
  224. if requested_filename not in [VERIFIED_REQ]:
  225. raise SmbException(STATUS_NO_SUCH_FILE, "Couldn't find the file")
  226. fid, filename = tempfile.mkstemp()
  227. log.debug("[SMB] Created %s (%d) for storing '%s'",
  228. filename, fid, requested_filename)
  229. contents = ""
  230. if requested_filename == VERIFIED_REQ:
  231. log.debug("[SMB] Verifying server is alive")
  232. pid = os.getpid()
  233. # see tests/server/util.c function write_pidfile
  234. if os.name == "nt":
  235. pid += 65536
  236. contents = VERIFIED_RSP.format(pid=pid).encode('utf-8')
  237. self.write_to_fid(fid, contents)
  238. return fid, filename
  239. def write_to_fid(self, fid, contents):
  240. # Write the contents to file descriptor
  241. os.write(fid, contents)
  242. os.fsync(fid)
  243. # Rewind the file to the beginning so a read gets us the contents
  244. os.lseek(fid, 0, os.SEEK_SET)
  245. def get_test_path(self, requested_filename):
  246. log.info("[SMB] Get reply data from 'test%s'", requested_filename)
  247. fid, filename = tempfile.mkstemp()
  248. log.debug("[SMB] Created %s (%d) for storing test '%s'",
  249. filename, fid, requested_filename)
  250. try:
  251. contents = self.ctd.get_test_data(requested_filename).encode('utf-8')
  252. self.write_to_fid(fid, contents)
  253. return fid, filename
  254. except Exception:
  255. log.exception("Failed to make test file")
  256. raise SmbException(STATUS_NO_SUCH_FILE, "Failed to make test file")
  257. class SmbException(Exception):
  258. def __init__(self, error_code, error_message):
  259. super(SmbException, self).__init__(error_message)
  260. self.error_code = error_code
  261. class ScriptRC(object):
  262. """Enum for script return codes"""
  263. SUCCESS = 0
  264. FAILURE = 1
  265. EXCEPTION = 2
  266. class ScriptException(Exception):
  267. pass
  268. def get_options():
  269. parser = argparse.ArgumentParser()
  270. parser.add_argument("--port", action="store", default=9017,
  271. type=int, help="port to listen on")
  272. parser.add_argument("--host", action="store", default="127.0.0.1",
  273. help="host to listen on")
  274. parser.add_argument("--verbose", action="store", type=int, default=0,
  275. help="verbose output")
  276. parser.add_argument("--pidfile", action="store",
  277. help="file name for the PID")
  278. parser.add_argument("--logfile", action="store",
  279. help="file name for the log")
  280. parser.add_argument("--srcdir", action="store", help="test directory")
  281. parser.add_argument("--id", action="store", help="server ID")
  282. parser.add_argument("--ipv4", action="store_true", default=0,
  283. help="IPv4 flag")
  284. return parser.parse_args()
  285. def setup_logging(options):
  286. """
  287. Set up logging from the command line options
  288. """
  289. root_logger = logging.getLogger()
  290. add_stdout = False
  291. formatter = logging.Formatter("%(asctime)s %(levelname)-5.5s %(message)s")
  292. # Write out to a logfile
  293. if options.logfile:
  294. handler = ClosingFileHandler(options.logfile)
  295. handler.setFormatter(formatter)
  296. handler.setLevel(logging.DEBUG)
  297. root_logger.addHandler(handler)
  298. else:
  299. # The logfile wasn't specified. Add a stdout logger.
  300. add_stdout = True
  301. if options.verbose:
  302. # Add a stdout logger as well in verbose mode
  303. root_logger.setLevel(logging.DEBUG)
  304. add_stdout = True
  305. else:
  306. root_logger.setLevel(logging.INFO)
  307. if add_stdout:
  308. stdout_handler = logging.StreamHandler(sys.stdout)
  309. stdout_handler.setFormatter(formatter)
  310. stdout_handler.setLevel(logging.DEBUG)
  311. root_logger.addHandler(stdout_handler)
  312. if __name__ == '__main__':
  313. # Get the options from the user.
  314. options = get_options()
  315. # Setup logging using the user options
  316. setup_logging(options)
  317. # Run main script.
  318. try:
  319. rc = smbserver(options)
  320. except Exception as e:
  321. log.exception(e)
  322. rc = ScriptRC.EXCEPTION
  323. if options.pidfile and os.path.isfile(options.pidfile):
  324. os.unlink(options.pidfile)
  325. log.info("[SMB] Returning %d", rc)
  326. sys.exit(rc)