Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

unix.py 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. # Copyright (c) Twisted Matrix Laboratories.
  2. # See LICENSE for details.
  3. """
  4. A UNIX SSH server.
  5. """
  6. import fcntl
  7. import grp
  8. import os
  9. import pty
  10. import pwd
  11. import socket
  12. import struct
  13. import time
  14. import tty
  15. from zope.interface import implementer
  16. from twisted.conch import ttymodes
  17. from twisted.conch.avatar import ConchUser
  18. from twisted.conch.error import ConchError
  19. from twisted.conch.interfaces import ISession, ISFTPFile, ISFTPServer
  20. from twisted.conch.ls import lsLine
  21. from twisted.conch.ssh import filetransfer, forwarding, session
  22. from twisted.conch.ssh.filetransfer import (
  23. FXF_APPEND,
  24. FXF_CREAT,
  25. FXF_EXCL,
  26. FXF_READ,
  27. FXF_TRUNC,
  28. FXF_WRITE,
  29. )
  30. from twisted.cred import portal
  31. from twisted.internet.error import ProcessExitedAlready
  32. from twisted.logger import Logger
  33. from twisted.python import components
  34. from twisted.python.compat import nativeString
  35. try:
  36. import utmp # type: ignore[import]
  37. except ImportError:
  38. utmp = None
  39. @implementer(portal.IRealm)
  40. class UnixSSHRealm:
  41. def requestAvatar(self, username, mind, *interfaces):
  42. user = UnixConchUser(username)
  43. return interfaces[0], user, user.logout
  44. class UnixConchUser(ConchUser):
  45. def __init__(self, username):
  46. ConchUser.__init__(self)
  47. self.username = username
  48. self.pwdData = pwd.getpwnam(self.username)
  49. l = [self.pwdData[3]]
  50. for groupname, password, gid, userlist in grp.getgrall():
  51. if username in userlist:
  52. l.append(gid)
  53. self.otherGroups = l
  54. self.listeners = {} # Dict mapping (interface, port) -> listener
  55. self.channelLookup.update(
  56. {
  57. b"session": session.SSHSession,
  58. b"direct-tcpip": forwarding.openConnectForwardingClient,
  59. }
  60. )
  61. self.subsystemLookup.update({b"sftp": filetransfer.FileTransferServer})
  62. def getUserGroupId(self):
  63. return self.pwdData[2:4]
  64. def getOtherGroups(self):
  65. return self.otherGroups
  66. def getHomeDir(self):
  67. return self.pwdData[5]
  68. def getShell(self):
  69. return self.pwdData[6]
  70. def global_tcpip_forward(self, data):
  71. hostToBind, portToBind = forwarding.unpackGlobal_tcpip_forward(data)
  72. from twisted.internet import reactor
  73. try:
  74. listener = self._runAsUser(
  75. reactor.listenTCP,
  76. portToBind,
  77. forwarding.SSHListenForwardingFactory(
  78. self.conn,
  79. (hostToBind, portToBind),
  80. forwarding.SSHListenServerForwardingChannel,
  81. ),
  82. interface=hostToBind,
  83. )
  84. except BaseException:
  85. return 0
  86. else:
  87. self.listeners[(hostToBind, portToBind)] = listener
  88. if portToBind == 0:
  89. portToBind = listener.getHost()[2] # The port
  90. return 1, struct.pack(">L", portToBind)
  91. else:
  92. return 1
  93. def global_cancel_tcpip_forward(self, data):
  94. hostToBind, portToBind = forwarding.unpackGlobal_tcpip_forward(data)
  95. listener = self.listeners.get((hostToBind, portToBind), None)
  96. if not listener:
  97. return 0
  98. del self.listeners[(hostToBind, portToBind)]
  99. self._runAsUser(listener.stopListening)
  100. return 1
  101. def logout(self):
  102. # Remove all listeners.
  103. for listener in self.listeners.values():
  104. self._runAsUser(listener.stopListening)
  105. self._log.info(
  106. "avatar {username} logging out ({nlisteners})",
  107. username=self.username,
  108. nlisteners=len(self.listeners),
  109. )
  110. def _runAsUser(self, f, *args, **kw):
  111. euid = os.geteuid()
  112. egid = os.getegid()
  113. groups = os.getgroups()
  114. uid, gid = self.getUserGroupId()
  115. os.setegid(0)
  116. os.seteuid(0)
  117. os.setgroups(self.getOtherGroups())
  118. os.setegid(gid)
  119. os.seteuid(uid)
  120. try:
  121. f = iter(f)
  122. except TypeError:
  123. f = [(f, args, kw)]
  124. try:
  125. for i in f:
  126. func = i[0]
  127. args = len(i) > 1 and i[1] or ()
  128. kw = len(i) > 2 and i[2] or {}
  129. r = func(*args, **kw)
  130. finally:
  131. os.setegid(0)
  132. os.seteuid(0)
  133. os.setgroups(groups)
  134. os.setegid(egid)
  135. os.seteuid(euid)
  136. return r
  137. @implementer(ISession)
  138. class SSHSessionForUnixConchUser:
  139. _log = Logger()
  140. def __init__(self, avatar, reactor=None):
  141. """
  142. Construct an C{SSHSessionForUnixConchUser}.
  143. @param avatar: The L{UnixConchUser} for whom this is an SSH session.
  144. @param reactor: An L{IReactorProcess} used to handle shell and exec
  145. requests. Uses the default reactor if None.
  146. """
  147. if reactor is None:
  148. from twisted.internet import reactor
  149. self._reactor = reactor
  150. self.avatar = avatar
  151. self.environ = {"PATH": "/bin:/usr/bin:/usr/local/bin"}
  152. self.pty = None
  153. self.ptyTuple = 0
  154. def addUTMPEntry(self, loggedIn=1):
  155. if not utmp:
  156. return
  157. ipAddress = self.avatar.conn.transport.transport.getPeer().host
  158. (packedIp,) = struct.unpack("L", socket.inet_aton(ipAddress))
  159. ttyName = self.ptyTuple[2][5:]
  160. t = time.time()
  161. t1 = int(t)
  162. t2 = int((t - t1) * 1e6)
  163. entry = utmp.UtmpEntry()
  164. entry.ut_type = loggedIn and utmp.USER_PROCESS or utmp.DEAD_PROCESS
  165. entry.ut_pid = self.pty.pid
  166. entry.ut_line = ttyName
  167. entry.ut_id = ttyName[-4:]
  168. entry.ut_tv = (t1, t2)
  169. if loggedIn:
  170. entry.ut_user = self.avatar.username
  171. entry.ut_host = socket.gethostbyaddr(ipAddress)[0]
  172. entry.ut_addr_v6 = (packedIp, 0, 0, 0)
  173. a = utmp.UtmpRecord(utmp.UTMP_FILE)
  174. a.pututline(entry)
  175. a.endutent()
  176. b = utmp.UtmpRecord(utmp.WTMP_FILE)
  177. b.pututline(entry)
  178. b.endutent()
  179. def getPty(self, term, windowSize, modes):
  180. self.environ["TERM"] = term
  181. self.winSize = windowSize
  182. self.modes = modes
  183. master, slave = pty.openpty()
  184. ttyname = os.ttyname(slave)
  185. self.environ["SSH_TTY"] = ttyname
  186. self.ptyTuple = (master, slave, ttyname)
  187. def openShell(self, proto):
  188. if not self.ptyTuple: # We didn't get a pty-req.
  189. self._log.error("tried to get shell without pty, failing")
  190. raise ConchError("no pty")
  191. uid, gid = self.avatar.getUserGroupId()
  192. homeDir = self.avatar.getHomeDir()
  193. shell = self.avatar.getShell()
  194. self.environ["USER"] = self.avatar.username
  195. self.environ["HOME"] = homeDir
  196. self.environ["SHELL"] = shell
  197. shellExec = os.path.basename(shell)
  198. peer = self.avatar.conn.transport.transport.getPeer()
  199. host = self.avatar.conn.transport.transport.getHost()
  200. self.environ["SSH_CLIENT"] = f"{peer.host} {peer.port} {host.port}"
  201. self.getPtyOwnership()
  202. self.pty = self._reactor.spawnProcess(
  203. proto,
  204. shell,
  205. [f"-{shellExec}"],
  206. self.environ,
  207. homeDir,
  208. uid,
  209. gid,
  210. usePTY=self.ptyTuple,
  211. )
  212. self.addUTMPEntry()
  213. fcntl.ioctl(self.pty.fileno(), tty.TIOCSWINSZ, struct.pack("4H", *self.winSize))
  214. if self.modes:
  215. self.setModes()
  216. self.oldWrite = proto.transport.write
  217. proto.transport.write = self._writeHack
  218. self.avatar.conn.transport.transport.setTcpNoDelay(1)
  219. def execCommand(self, proto, cmd):
  220. uid, gid = self.avatar.getUserGroupId()
  221. homeDir = self.avatar.getHomeDir()
  222. shell = self.avatar.getShell() or "/bin/sh"
  223. self.environ["HOME"] = homeDir
  224. command = (shell, "-c", cmd)
  225. peer = self.avatar.conn.transport.transport.getPeer()
  226. host = self.avatar.conn.transport.transport.getHost()
  227. self.environ["SSH_CLIENT"] = f"{peer.host} {peer.port} {host.port}"
  228. if self.ptyTuple:
  229. self.getPtyOwnership()
  230. self.pty = self._reactor.spawnProcess(
  231. proto,
  232. shell,
  233. command,
  234. self.environ,
  235. homeDir,
  236. uid,
  237. gid,
  238. usePTY=self.ptyTuple or 0,
  239. )
  240. if self.ptyTuple:
  241. self.addUTMPEntry()
  242. if self.modes:
  243. self.setModes()
  244. self.avatar.conn.transport.transport.setTcpNoDelay(1)
  245. def getPtyOwnership(self):
  246. ttyGid = os.stat(self.ptyTuple[2])[5]
  247. uid, gid = self.avatar.getUserGroupId()
  248. euid, egid = os.geteuid(), os.getegid()
  249. os.setegid(0)
  250. os.seteuid(0)
  251. try:
  252. os.chown(self.ptyTuple[2], uid, ttyGid)
  253. finally:
  254. os.setegid(egid)
  255. os.seteuid(euid)
  256. def setModes(self):
  257. pty = self.pty
  258. attr = tty.tcgetattr(pty.fileno())
  259. for mode, modeValue in self.modes:
  260. if mode not in ttymodes.TTYMODES:
  261. continue
  262. ttyMode = ttymodes.TTYMODES[mode]
  263. if len(ttyMode) == 2: # Flag.
  264. flag, ttyAttr = ttyMode
  265. if not hasattr(tty, ttyAttr):
  266. continue
  267. ttyval = getattr(tty, ttyAttr)
  268. if modeValue:
  269. attr[flag] = attr[flag] | ttyval
  270. else:
  271. attr[flag] = attr[flag] & ~ttyval
  272. elif ttyMode == "OSPEED":
  273. attr[tty.OSPEED] = getattr(tty, f"B{modeValue}")
  274. elif ttyMode == "ISPEED":
  275. attr[tty.ISPEED] = getattr(tty, f"B{modeValue}")
  276. else:
  277. if not hasattr(tty, ttyMode):
  278. continue
  279. ttyval = getattr(tty, ttyMode)
  280. attr[tty.CC][ttyval] = bytes((modeValue,))
  281. tty.tcsetattr(pty.fileno(), tty.TCSANOW, attr)
  282. def eofReceived(self):
  283. if self.pty:
  284. self.pty.closeStdin()
  285. def closed(self):
  286. if self.ptyTuple and os.path.exists(self.ptyTuple[2]):
  287. ttyGID = os.stat(self.ptyTuple[2])[5]
  288. os.chown(self.ptyTuple[2], 0, ttyGID)
  289. if self.pty:
  290. try:
  291. self.pty.signalProcess("HUP")
  292. except (OSError, ProcessExitedAlready):
  293. pass
  294. self.pty.loseConnection()
  295. self.addUTMPEntry(0)
  296. self._log.info("shell closed")
  297. def windowChanged(self, winSize):
  298. self.winSize = winSize
  299. fcntl.ioctl(self.pty.fileno(), tty.TIOCSWINSZ, struct.pack("4H", *self.winSize))
  300. def _writeHack(self, data):
  301. """
  302. Hack to send ignore messages when we aren't echoing.
  303. """
  304. if self.pty is not None:
  305. attr = tty.tcgetattr(self.pty.fileno())[3]
  306. if not attr & tty.ECHO and attr & tty.ICANON: # No echo.
  307. self.avatar.conn.transport.sendIgnore("\x00" * (8 + len(data)))
  308. self.oldWrite(data)
  309. @implementer(ISFTPServer)
  310. class SFTPServerForUnixConchUser:
  311. def __init__(self, avatar):
  312. self.avatar = avatar
  313. def _setAttrs(self, path, attrs):
  314. """
  315. NOTE: this function assumes it runs as the logged-in user:
  316. i.e. under _runAsUser()
  317. """
  318. if "uid" in attrs and "gid" in attrs:
  319. os.chown(path, attrs["uid"], attrs["gid"])
  320. if "permissions" in attrs:
  321. os.chmod(path, attrs["permissions"])
  322. if "atime" in attrs and "mtime" in attrs:
  323. os.utime(path, (attrs["atime"], attrs["mtime"]))
  324. def _getAttrs(self, s):
  325. return {
  326. "size": s.st_size,
  327. "uid": s.st_uid,
  328. "gid": s.st_gid,
  329. "permissions": s.st_mode,
  330. "atime": int(s.st_atime),
  331. "mtime": int(s.st_mtime),
  332. }
  333. def _absPath(self, path):
  334. home = self.avatar.getHomeDir()
  335. return os.path.join(nativeString(home.path), nativeString(path))
  336. def gotVersion(self, otherVersion, extData):
  337. return {}
  338. def openFile(self, filename, flags, attrs):
  339. return UnixSFTPFile(self, self._absPath(filename), flags, attrs)
  340. def removeFile(self, filename):
  341. filename = self._absPath(filename)
  342. return self.avatar._runAsUser(os.remove, filename)
  343. def renameFile(self, oldpath, newpath):
  344. oldpath = self._absPath(oldpath)
  345. newpath = self._absPath(newpath)
  346. return self.avatar._runAsUser(os.rename, oldpath, newpath)
  347. def makeDirectory(self, path, attrs):
  348. path = self._absPath(path)
  349. return self.avatar._runAsUser(
  350. [(os.mkdir, (path,)), (self._setAttrs, (path, attrs))]
  351. )
  352. def removeDirectory(self, path):
  353. path = self._absPath(path)
  354. self.avatar._runAsUser(os.rmdir, path)
  355. def openDirectory(self, path):
  356. return UnixSFTPDirectory(self, self._absPath(path))
  357. def getAttrs(self, path, followLinks):
  358. path = self._absPath(path)
  359. if followLinks:
  360. s = self.avatar._runAsUser(os.stat, path)
  361. else:
  362. s = self.avatar._runAsUser(os.lstat, path)
  363. return self._getAttrs(s)
  364. def setAttrs(self, path, attrs):
  365. path = self._absPath(path)
  366. self.avatar._runAsUser(self._setAttrs, path, attrs)
  367. def readLink(self, path):
  368. path = self._absPath(path)
  369. return self.avatar._runAsUser(os.readlink, path)
  370. def makeLink(self, linkPath, targetPath):
  371. linkPath = self._absPath(linkPath)
  372. targetPath = self._absPath(targetPath)
  373. return self.avatar._runAsUser(os.symlink, targetPath, linkPath)
  374. def realPath(self, path):
  375. return os.path.realpath(self._absPath(path))
  376. def extendedRequest(self, extName, extData):
  377. raise NotImplementedError
  378. @implementer(ISFTPFile)
  379. class UnixSFTPFile:
  380. def __init__(self, server, filename, flags, attrs):
  381. self.server = server
  382. openFlags = 0
  383. if flags & FXF_READ == FXF_READ and flags & FXF_WRITE == 0:
  384. openFlags = os.O_RDONLY
  385. if flags & FXF_WRITE == FXF_WRITE and flags & FXF_READ == 0:
  386. openFlags = os.O_WRONLY
  387. if flags & FXF_WRITE == FXF_WRITE and flags & FXF_READ == FXF_READ:
  388. openFlags = os.O_RDWR
  389. if flags & FXF_APPEND == FXF_APPEND:
  390. openFlags |= os.O_APPEND
  391. if flags & FXF_CREAT == FXF_CREAT:
  392. openFlags |= os.O_CREAT
  393. if flags & FXF_TRUNC == FXF_TRUNC:
  394. openFlags |= os.O_TRUNC
  395. if flags & FXF_EXCL == FXF_EXCL:
  396. openFlags |= os.O_EXCL
  397. if "permissions" in attrs:
  398. mode = attrs["permissions"]
  399. del attrs["permissions"]
  400. else:
  401. mode = 0o777
  402. fd = server.avatar._runAsUser(os.open, filename, openFlags, mode)
  403. if attrs:
  404. server.avatar._runAsUser(server._setAttrs, filename, attrs)
  405. self.fd = fd
  406. def close(self):
  407. return self.server.avatar._runAsUser(os.close, self.fd)
  408. def readChunk(self, offset, length):
  409. return self.server.avatar._runAsUser(
  410. [(os.lseek, (self.fd, offset, 0)), (os.read, (self.fd, length))]
  411. )
  412. def writeChunk(self, offset, data):
  413. return self.server.avatar._runAsUser(
  414. [(os.lseek, (self.fd, offset, 0)), (os.write, (self.fd, data))]
  415. )
  416. def getAttrs(self):
  417. s = self.server.avatar._runAsUser(os.fstat, self.fd)
  418. return self.server._getAttrs(s)
  419. def setAttrs(self, attrs):
  420. raise NotImplementedError
  421. class UnixSFTPDirectory:
  422. def __init__(self, server, directory):
  423. self.server = server
  424. self.files = server.avatar._runAsUser(os.listdir, directory)
  425. self.dir = directory
  426. def __iter__(self):
  427. return self
  428. def __next__(self):
  429. try:
  430. f = self.files.pop(0)
  431. except IndexError:
  432. raise StopIteration
  433. else:
  434. s = self.server.avatar._runAsUser(os.lstat, os.path.join(self.dir, f))
  435. longname = lsLine(f, s)
  436. attrs = self.server._getAttrs(s)
  437. return (f, longname, attrs)
  438. next = __next__
  439. def close(self):
  440. self.files = []
  441. components.registerAdapter(
  442. SFTPServerForUnixConchUser, UnixConchUser, filetransfer.ISFTPServer
  443. )
  444. components.registerAdapter(SSHSessionForUnixConchUser, UnixConchUser, session.ISession)