unix.py 16 KB

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