├── .gear └── rules ├── MANIFEST.in ├── .gitignore ├── test ├── analyze.py └── test.py ├── py9p ├── utils.py ├── __init__.py.in ├── pki.py ├── fuse9p.py └── py9p.py ├── LICENSE ├── README.md ├── configure.gawk ├── 9pfs ├── 9pfs.1 └── 9pfs ├── Makefile ├── setup.py.in ├── fuse9p ├── fuse9p.1 └── fuse9p └── examples ├── simplesrv.py ├── composite.py └── cl.py /.gear/rules: -------------------------------------------------------------------------------- 1 | tar: . 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include python-py9p.spec 2 | include README* 3 | include LICENSE 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *~ 4 | *swp 5 | _build 6 | MANIFEST 7 | dist/ 8 | setup.py 9 | py9p/__init__.py 10 | -------------------------------------------------------------------------------- /test/analyze.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import pstats 4 | import sys 5 | 6 | if len(sys.argv) > 1: 7 | fname = sys.argv[1] 8 | else: 9 | fname = "profile.stats" 10 | 11 | p = pstats.Stats(fname) 12 | p.strip_dirs() 13 | p.sort_stats("time") 14 | p.print_stats() 15 | 16 | -------------------------------------------------------------------------------- /py9p/utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | if sys.version_info[0] == 2: 4 | def bytes3(x): 5 | if isinstance(x, unicode): 6 | return bytes(x.encode('utf-8')) 7 | else: 8 | return bytes(x) 9 | else: 10 | def bytes3(x): 11 | return bytes(x, 'utf-8') 12 | 13 | 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008-2011 Tim Newsham, Andrey Mirtchovski 2 | Copyright (c) 2011-2012 Peter V. Saveliev 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /py9p/__init__.py.in: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011-2012 Peter V. Saveliev 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining 4 | # a copy of this software and associated documentation files (the 5 | # "Software"), to deal in the Software without restriction, including 6 | # without limitation the rights to use, copy, modify, merge, publish, 7 | # distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so, subject to 9 | # the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be 12 | # included in all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | __version__ = "@RELEASE@" 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | project EOL notice 2 | ================== 3 | 4 | This project is no longer maintained. Plan9 9p2000 protocol implementation 5 | is completely rewritten and provided in `pyroute2`, see the links: 6 | 7 | * project: https://github.com/svinota/pyroute2 8 | * module: https://github.com/svinota/pyroute2/tree/master/pyroute2/plan9 9 | * documentation and examples: https://docs.pyroute2.org/plan9.html 10 | 11 | 12 | py9p 13 | ==== 14 | 15 | The code is based on Andrey Mirtchovski's py9p, but differs in some things: 16 | 17 | * the protocol implementation is faster up to 3 times 18 | * thread-safe 19 | * python3 support 20 | * has no sk1 support — temporarily; it will be added back soon 21 | * has working pki support for RSA ssh keys 22 | * FUSE client 23 | 24 | Fuse client (fuse9p) has features not provided by other mount implementations 25 | like 9pfuse from «Plan9 from User Space» or Linux kernel's v9fs. Firstly, 26 | fuse9p supports authentication (right now only pki). Having comparable speed 27 | with v9fs on big read/write requests, it is up to several hundreds times faster 28 | in reading directories. And will be more faster in the future :) 29 | 30 | * Documentation: none yet, working on it 31 | * Installation: make install 32 | * Requirements: Python >= 2.6 33 | 34 | You can also use the library without installation, but in this case you 35 | should set up PYTHONPATH manually and run `make force-version` to update 36 | all \*.in files. 37 | 38 | -------------------------------------------------------------------------------- /test/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import socket 3 | import sys 4 | import os 5 | import timeit 6 | import py9p 7 | 8 | try: 9 | from py9p import __version__ as ng 10 | assert ng > "1.0.6" 11 | from py9p import py9p 12 | assert hasattr(py9p, "Credentials") 13 | except Exception as e: 14 | import traceback 15 | traceback.print_exc() 16 | ng = False 17 | 18 | class CmdClient(py9p.Client): 19 | 20 | def cat(self, name, out=None): 21 | self.open(name) 22 | self.read(self.msize) 23 | self.close() 24 | 25 | if __name__ == "__main__": 26 | 27 | if 'USER' in os.environ: 28 | user = os.environ['USER'] 29 | 30 | sock = socket.socket(socket.AF_INET) 31 | try: 32 | sock.connect(('localhost', 10001),) 33 | except socket.error as e: 34 | print("%s" % ( e.args[1])) 35 | sys.exit(255) 36 | 37 | if ng: 38 | print("testing py9p.ng version %s" % (ng)) 39 | cl = CmdClient(sock, py9p.Credentials(user), None) 40 | else: 41 | print("testing initial py9p") 42 | cl = CmdClient(py9p.Sock(sock, 0, 0), 'none', user, None, None, 0) 43 | 44 | if len(sys.argv) > 1 and sys.argv[1] == "profile": 45 | for x in range(1000): 46 | cl.cat("sample1") 47 | else: 48 | t = timeit.Timer('cl.cat("sample1")','from __main__ import cl') 49 | print("1000 cats (walk/open/read/clunk) in %s seconds" % 50 | (t.timeit(1000))) 51 | -------------------------------------------------------------------------------- /configure.gawk: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011-2012 Peter V. Saveliev 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining 4 | # a copy of this software and associated documentation files (the 5 | # "Software"), to deal in the Software without restriction, including 6 | # without limitation the rights to use, copy, modify, merge, publish, 7 | # distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so, subject to 9 | # the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be 12 | # included in all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | BEGIN { 23 | conf["VERSION"] = version 24 | conf["RELEASE"] = release 25 | conf["alt", "PACKAGER"] = "Peter V. Saveliev " 26 | conf["rh", "PACKAGER"] = "Peter V. Saveliev " 27 | } 28 | 29 | { 30 | while (1) { 31 | # pick one variable 32 | variable = gensub(/.*@([^@]*)@.*/,"\\1",1) 33 | # no more variables left 34 | if (variable == $0) break 35 | # value lookup: 36 | if (conf[flavor, variable]) { 37 | # dist-specific 38 | value = conf[flavor, variable] 39 | } else { 40 | # common variables 41 | value = conf[variable] 42 | } 43 | # substitute the variable 44 | gsub("@"variable"@", value) 45 | } 46 | print $0 47 | } 48 | -------------------------------------------------------------------------------- /9pfs/9pfs.1: -------------------------------------------------------------------------------- 1 | .TH "9pfs" "1" "" "Peter V. Saveliev " "" 2 | .SH "NAME" 3 | 9pfs \- 9p2000 file server 4 | .SH "SYNOPSIS" 5 | \fB9pfs\fR [\-dDw] [\-c mode] [\-p port] [\-r root] [\-a address] [user [domain]] 6 | 7 | .SH "DESCRIPTION" 8 | 9p2000 is a file/RPC protocol developed for Plan9 operationg system. 9 | Due to its extreme simplicity it can be used to embed file servers in 10 | different applications to provide access to the internal structures 11 | and API in runtime. 12 | 13 | 9pfs is a simle file server that exports a file tree with 9p2000 14 | protocol. 15 | 16 | .SH "OPTIONS" 17 | \fB\-a\fR address 18 | .br 19 | Address to listen on. Default: 0.0.0.0 20 | 21 | \fB\-c\fR mode 22 | .br 23 | Authentication mode. Can be \fBpki\fR or \fBsk1\fR. 24 | 25 | \fB\-d\fR 26 | .br 27 | Turn on debug. 28 | 29 | \fB\-D\fR 30 | .br 31 | Turn on .u extensions, required for symlink support. 32 | 33 | \fB\-p\fR port 34 | .br 35 | Server TCP port, if it differs from the default 9p. 36 | 37 | \fB\-r\fR root 38 | .br 39 | A directory to export. 40 | 41 | \fB\-w\fR 42 | .br 43 | Allow read/write access. Default: read/only. 44 | 45 | 46 | .SH "AUTHENTICATION" 47 | \fBpki mode\fR 48 | 49 | PKI authentication mode uses standard SSH RSA keys. The server looks for 50 | the public key in /home/${user}/.ssh/id_rsa.pub. 51 | 52 | \fBsk1 mode\fR 53 | 54 | \fBdomain\fR should be specified only for sk1 auth mode. 55 | 56 | .SH "SEE ALSO" 57 | \fBssh\-keygen\fR(1), \fBfuse9p\fR(1) 58 | 59 | 60 | .SH "AUTHORS" 61 | 62 | \fB*\fR Peter V. Saveliev \fB\fR \-\- fuse9p author, py9p library maintainer 63 | \fB*\fR Andrey Mirtchovski \fB\fR \-\- py9p library author 64 | 65 | 66 | .SH "LINKS" 67 | \fB*\fR All bugs post to the project page: 68 | .br 69 | \fBhttps://github.com/svinota/py9p/issues\fR 70 | .br 71 | \fB*\fR Project's home: 72 | .br 73 | \fBhttps://github.com/svinota/py9p/\fR 74 | .br 75 | \fB*\fR 9p specifications: 76 | .br 77 | \fBhttp://swtch.com/plan9port/man/man9/\fR 78 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011-2012 Peter V. Saveliev 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining 4 | # a copy of this software and associated documentation files (the 5 | # "Software"), to deal in the Software without restriction, including 6 | # without limitation the rights to use, copy, modify, merge, publish, 7 | # distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so, subject to 9 | # the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be 12 | # included in all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | version ?= "1.0" 23 | release ?= "1.0.9" 24 | python ?= "python" 25 | 26 | ifdef root 27 | override root := "--root=${root}" 28 | endif 29 | 30 | ifdef lib 31 | override lib := "--install-lib=${lib}" 32 | endif 33 | 34 | 35 | all: 36 | @echo targets: dist, install 37 | 38 | clean: clean-version 39 | rm -rf dist build MANIFEST 40 | find . -name "*pyc" -exec rm -f "{}" \; 41 | 42 | check: 43 | for i in py9p fuse9p/fuse9p 9pfs/9pfs; \ 44 | do pep8 $$i || exit 1; \ 45 | pyflakes $$i || exit 2; \ 46 | done 47 | 48 | setup.py py9p/__init__.py: 49 | gawk -v version=${version} -v release=${release} -v flavor=${flavor}\ 50 | -f configure.gawk $@.in >$@ 51 | 52 | clean-version: 53 | rm -f setup.py 54 | rm -f py9p/__init__.py 55 | 56 | update-version: setup.py py9p/__init__.py 57 | 58 | force-version: clean-version update-version 59 | 60 | docs: clean force-version 61 | make -C docs html 62 | 63 | dist: clean force-version 64 | ${python} setup.py sdist 65 | 66 | upload: clean force-version 67 | ${python} setup.py sdist upload 68 | 69 | rpm: dist 70 | rpmbuild -ta dist/*tar.gz 71 | 72 | install: clean force-version 73 | ${python} setup.py install ${root} ${lib} 74 | 75 | -------------------------------------------------------------------------------- /setup.py.in: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2011-2012 Peter V. Saveliev 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining 6 | # a copy of this software and associated documentation files (the 7 | # "Software"), to deal in the Software without restriction, including 8 | # without limitation the rights to use, copy, modify, merge, publish, 9 | # distribute, sublicense, and/or sell copies of the Software, and to 10 | # permit persons to whom the Software is furnished to do so, subject to 11 | # the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | from distutils.core import setup 25 | 26 | man1dir="/usr/share/man/man1" 27 | 28 | setup(name='py9p', 29 | version='@RELEASE@', 30 | description='9P Protocol Implementation', 31 | author='Andrey Mirtchovski', 32 | author_email='aamirtch@ucalgary.ca', 33 | maintainer='Peter V. Saveliev', 34 | maintainer_email='peet@redhat.com', 35 | url='https://github.com/svinota/py9p', 36 | license="MIT", 37 | packages=[ 38 | 'py9p' 39 | ], 40 | scripts=[ 41 | 'fuse9p/fuse9p', 42 | '9pfs/9pfs', 43 | ], 44 | data_files=[ 45 | (man1dir, ['fuse9p/fuse9p.1',]), 46 | (man1dir, ['9pfs/9pfs.1',]), 47 | ], 48 | classifiers=[ 49 | 'License :: OSI Approved :: MIT License', 50 | 'Programming Language :: Python', 51 | 'Topic :: Software Development :: Libraries :: Python Modules', 52 | 'Operating System :: POSIX', 53 | 'Intended Audience :: Developers', 54 | 'Development Status :: 4 - Beta', 55 | ], 56 | long_description=''' 57 | 9P protocol implementation 58 | ========================== 59 | 60 | The library allows you to use 9P protocol in your 61 | applications. Please note, that the library is not 62 | fully compatible with the original version by 63 | Andrey Mirtchovski. 64 | 65 | Also, this package provides two components: 66 | 67 | * fuse9p -- FUSE 9p client 68 | * 9pfs -- simple file server (alpha state) 69 | 70 | Links 71 | ===== 72 | 73 | * home: https://github.com/svinota/py9p 74 | * bugs: https://github.com/svinota/py9p/issues 75 | * pypi: http://pypi.python.org/pypi/py9p/ 76 | 77 | Changes 78 | ======= 79 | 80 | 1.0.8 -- Neoarchean 81 | ------------------- 82 | 83 | * fuse9p: fid cache fixed 84 | * fuse9p: several thread safety issues fixed 85 | * fuse9p: iounit negotiation 86 | * fuse9p: support rename() routine 87 | * fuse9p: uid/gid map feature 88 | 89 | 1.0.7 -- Mesoarchean 90 | -------------------- 91 | 92 | * PKI auth fixed 93 | * fuse9p: "persistent connection" feature, -P 94 | * fuse9p: symlink support 95 | * fuse9p: multiple fixes of the background mode 96 | * 9pfs: new component, that grow up from localfs 97 | * py9p: provide mode conversion routines 98 | 99 | 1.0.6 -- Paleoarchean 100 | --------------------- 101 | 102 | * Tcreate client call fixed 103 | * fuse9p client, supporting stateful I/O, 104 | "reconnect after network errors" and so on. 105 | 106 | 1.0.4 -- Eoarchaean 107 | ------------------- 108 | 109 | * support arbitrary key files for PKI 110 | 111 | 1.0.3 112 | ----- 113 | 114 | * initial pypi release 115 | ''' 116 | ) 117 | -------------------------------------------------------------------------------- /fuse9p/fuse9p.1: -------------------------------------------------------------------------------- 1 | .TH "fuse9p" "1" "" "Peter V. Saveliev " "" 2 | .SH "NAME" 3 | fuse9p \- filesystem client for 9p2000.u servers 4 | .SH "SYNOPSIS" 5 | \fBmounting\fR 6 | .br 7 | \fBfuse9p\fR [\-dPv] [\-c mode] [\-k file] [\-l user] [\-p port] [\-t secs] 8 | [\-U uid_map] [\-G gid_map] [user@]\fBserver\fR[:port] \fBmountpoint\fR 9 | 10 | \fBunmounting\fR 11 | .br 12 | \fBfusermount \-u mountpoint\fR 13 | .SH "DESCRIPTION" 14 | 9p2000 is a file/RPC protocol developed for Plan9 operationg system. Due to its extreme simplicity it can be used to embed file servers in different applications to provide access to the internal structures and API in runtime. 9p filesystem can be mounted as well with the kernel FS implementation, but the kernel v9fs module does not support client authentication. Exporting a read/write filesystem without any authentication is a serious issue. So, if you want to export FS with authentication enabled, you have to use a client that supports it, like this \fBfuse9p\fR implementation. 15 | 16 | Another difference from the kernel v9fs is a protocol optimizations that allow \fBfuse9p\fR to work faster, avoiding unnecessary request. 17 | .SH "OPTIONS" 18 | \fB\-c\fR mode 19 | .br 20 | Authentication mode. Now only \fBpki\fR mode is supported by fuse9p. 21 | 22 | \fB\-d\fR 23 | .br 24 | Turn on debug and run in foreground. Please note, that in this mode you can not stop \fBfuse9p\fR with Ctrl\-C, you should use \fBfusermount \-u\fR. 25 | 26 | \fB\-G\fR gid_map 27 | .br 28 | Turn on gid mapping (see \fBUID/GID MAPPING\fR below) 29 | 30 | \fB\-k\fR file 31 | .br 32 | Path to the private RSA key file. Implies \fB\-c pki\fR. 33 | 34 | \fB\-l\fR user 35 | .br 36 | User name to use in FS Tattach command. 37 | 38 | \fB\-p\fR port 39 | .br 40 | Server TCP port, if it differs from the default 9p. 41 | 42 | \fB\-P\fR 43 | .br 44 | Stay connected even in the case of network errors 45 | 46 | \fB\-t\fR secs 47 | .br 48 | Timeout (in seconds) for the 9p socket. By default it is 10 seconds. 49 | 50 | \fB\-G\fR gid_map 51 | .br 52 | Turn on gid mapping (see \fBUID/GID MAPPING\fR below) 53 | 54 | \fB\-v\fR 55 | .br 56 | Print py9p version 57 | 58 | 59 | .SH "LIMITATIONS" 60 | Current \fBfuse9p\fR implementation does not support: 61 | 62 | \fB*\fR named pipes 63 | .br 64 | \fB*\fR UNIX sockets 65 | .br 66 | \fB*\fR hard linking 67 | 68 | Any other functionality can be limited; if so, report an issue to the project's bugtracker. 69 | 70 | 71 | .SH "AUTHENTICATION" 72 | \fBpki mode\fR 73 | 74 | PKI authentication mode uses standard SSH RSA keys. The server should have the public one, the client should use the corresponding private key. If the private key file location is not set up by \fB\-k\fR option, \fBfuse9p\fR tries to load it from /home/${user}/.ssh/id_rsa. If the user is not set up by \fB\-l\fR option or in the server spec, \fBfuse9p\fR uses $USER environment variable. 75 | 76 | 77 | .SH "RECONNECTION" 78 | 79 | Being started with \fB\-P\fR option, \fBfuse9p\fR tries to reconnect to the 80 | server, if the connection is lost. When there is no connection, in this mode 81 | \fBfuse9p\fR provides empty mount point. All operations on open files will 82 | return EIO or ENOENT. 83 | 84 | \fBfuse9p\fR reconnect interval increases with each iteration, it grows by 85 | power of 2: 2, 4, 8, 16 etc. seconds up to the some limit. Each file stat() 86 | or directory listing call resets the interval back to 2 seconds. 87 | 88 | Since the reconnection is running asynchronously, you can get empty mount 89 | point even if the server became reachable; if so, just repeat the directory 90 | listing call. 91 | 92 | .SH "UID/GID MAPPING" 93 | 94 | Often, uids/gids on the server and client side are not the same. This feature 95 | allows you to map server's uids/gids into client's ones in the way like this: 96 | 97 | ... \fB\-U\fR "{1000: 500, 1001: 505}" \fB\-G\fR "{1000: 500}" ... 98 | 99 | Please note, that server's uid/gid goes first. 100 | 101 | .SH "SEE ALSO" 102 | \fBssh\-keygen\fR(1), \fB9pfs\fR 103 | 104 | 105 | .SH "AUTHORS" 106 | 107 | \fB*\fR Peter V. Saveliev \fB\fR \-\- fuse9p author, py9p library maintainer 108 | \fB*\fR Andrey Mirtchovski \fB\fR \-\- py9p library author 109 | 110 | 111 | .SH "LINKS" 112 | \fB*\fR All bugs post to the project page: 113 | .br 114 | \fBhttps://github.com/svinota/py9p/issues\fR 115 | .br 116 | \fB*\fR Project's home: 117 | .br 118 | \fBhttps://github.com/svinota/py9p/\fR 119 | .br 120 | \fB*\fR 9p specifications: 121 | .br 122 | \fBhttp://swtch.com/plan9port/man/man9/\fR 123 | -------------------------------------------------------------------------------- /fuse9p/fuse9p: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright (c) 2011-2012 Peter V. Saveliev 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining 6 | # a copy of this software and associated documentation files (the 7 | # "Software"), to deal in the Software without restriction, including 8 | # without limitation the rights to use, copy, modify, merge, publish, 9 | # distribute, sublicense, and/or sell copies of the Software, and to 10 | # permit persons to whom the Software is furnished to do so, subject to 11 | # the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | import os 25 | import sys 26 | import ast 27 | import getopt 28 | from py9p import py9p 29 | from py9p import fuse9p 30 | from py9p import __version__ as version 31 | 32 | 33 | errcodes = { 34 | "usage": (255, ""), 35 | "host": (254, "invalid host specification"), 36 | "port": (253, "invalid port specification"), 37 | "timeout": (252, "invalid timeout specification"), 38 | "key": (155, "key decryption error, probably bad password \ 39 | or wrong keyfile"), 40 | "socket": (154, "socket error"), 41 | "9connect": (153, "9p server connection error"), 42 | "undef": (100, "error")} 43 | 44 | 45 | def usage(): 46 | print(""" 47 | Usage: fuse9p [-dPv] [-c mode] [-k file] [-l user] [-p port] [-t secs] \ 48 | user@server:port mountpoint 49 | 50 | -c mode -- authentication mode to use (none|pki) 51 | -d -- turn on debug mode and run in foreground 52 | -k file -- path to the private RSA key for PKI (implies -c pki) 53 | -l user -- username to use in authentication 54 | -p port -- TCP port to use 55 | -t secs -- timeout for the socket 56 | -P -- stay connected even in the case of network errors 57 | -U map -- uid map 58 | -G map -- gid map 59 | -v -- print py9p version 60 | 61 | uid/gid maps format: {remote_uid: local_uid}, e.g.: 62 | ... -U "{1000: 500}" -G "{1000: 500}" ... 63 | (on Debian, user ids start from 1000, on RH -- from 500) 64 | """) 65 | 66 | 67 | def paluu(code, payload=None): 68 | print(errcodes[code][1]) 69 | if errcodes[code][0] > 200: 70 | usage() 71 | if payload is not None: 72 | print(str(payload)) 73 | sys.exit(errcodes[code][0]) 74 | 75 | 76 | prog = sys.argv[0] 77 | args = sys.argv[1:] 78 | port = py9p.PORT 79 | user = os.environ.get('USER', None) 80 | server = None 81 | mountpoint = None 82 | authmode = None 83 | keyfile = None 84 | debug = False 85 | timeout = 10 86 | keep_reconnect = False 87 | 88 | try: 89 | opts, args = getopt.getopt(args, "PdvU:G:c:k:l:p:t:") 90 | except: 91 | paluu("usage") 92 | 93 | for opt, optarg in opts: 94 | if opt == "-c": 95 | authmode = optarg 96 | elif opt == "-d": 97 | debug = True 98 | elif opt == "-k": 99 | authmode = "pki" 100 | keyfile = optarg 101 | elif opt == "-l": 102 | user = optarg 103 | elif opt == "-p": 104 | port = optarg 105 | elif opt == "-t": 106 | timeout = optarg 107 | elif opt == "-P": 108 | keep_reconnect = True 109 | elif opt == "-U": 110 | fuse9p.uid_map.update(ast.literal_eval(optarg)) 111 | elif opt == "-G": 112 | fuse9p.gid_map.update(ast.literal_eval(optarg)) 113 | elif opt == "-v": 114 | print("py9p version %s" % (version)) 115 | sys.exit(0) 116 | 117 | try: 118 | assert len(args) == 2 119 | except: 120 | paluu("usage") 121 | 122 | try: 123 | target = [] 124 | for x in args[0].split("@"): 125 | target.extend(x.split(":")) 126 | assert len(target) in (1, 2, 3) 127 | except: 128 | paluu("host") 129 | 130 | if len(target) == 3: 131 | user = target[0] 132 | server = target[1] 133 | else: 134 | server = target[0] 135 | 136 | try: 137 | if len(target) >= 2: 138 | port = target[-1] 139 | port = int(port) 140 | except: 141 | paluu("port") 142 | 143 | mountpoint = args[1] 144 | 145 | try: 146 | timeout = int(timeout) 147 | except: 148 | paluu("timeout") 149 | 150 | try: 151 | assert user is not None 152 | assert mountpoint is not None 153 | assert server is not None 154 | except: 155 | paluu("usage") 156 | 157 | try: 158 | credentials = py9p.Credentials(user, authmode, "", keyfile) 159 | except: 160 | paluu("key") 161 | 162 | try: 163 | fs = fuse9p.ClientFS((server, port), 164 | credentials, 165 | mountpoint, 166 | debug, 167 | timeout, 168 | keep_reconnect) 169 | fs.main() 170 | except py9p.Error as e: 171 | paluu("9connect", e) 172 | except Exception as e: 173 | paluu("undef", e) 174 | -------------------------------------------------------------------------------- /examples/simplesrv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import time 3 | import sys 4 | import getopt 5 | import os 6 | import copy 7 | from py9p import py9p 8 | 9 | import getopt 10 | import getpass 11 | 12 | 13 | if sys.version_info[0] == 2: 14 | def bytes3(x): 15 | return bytes(x) 16 | else: 17 | def bytes3(x): 18 | return bytes(x, 'utf-8') 19 | 20 | 21 | class SampleFs(py9p.Server): 22 | """ 23 | A sample plugin filesystem. 24 | """ 25 | mountpoint = '/' 26 | root = None 27 | files = {} 28 | def __init__(self): 29 | self.start = int(time.time()) 30 | rootdir = py9p.Dir(0) # not dotu 31 | rootdir.children = [] 32 | rootdir.type = 0 33 | rootdir.dev = 0 34 | rootdir.mode = 0o20000000755 35 | rootdir.atime = rootdir.mtime = int(time.time()) 36 | rootdir.length = 0 37 | rootdir.name = '/' 38 | rootdir.uid = rootdir.gid = rootdir.muid = bytes3(os.environ['USER']) 39 | rootdir.qid = py9p.Qid(py9p.QTDIR, 0, py9p.hash8(rootdir.name)) 40 | rootdir.parent = rootdir 41 | self.root = rootdir # / is its own parent, just so we don't fall off the edge of the earth 42 | 43 | # two files in '/' 44 | f = copy.copy(rootdir) 45 | f.name = 'sample1' 46 | f.qid = py9p.Qid(0, 0, py9p.hash8(f.name)) 47 | f.length = 1024 48 | f.parent = rootdir 49 | f.mode = 0o644 50 | self.root.children.append(f) 51 | f = copy.copy(f) 52 | f.name = 'sample2' 53 | f.length = 8192 54 | f.qid = py9p.Qid(0, 0, py9p.hash8(f.name)) 55 | f.mode = 0o644 56 | self.root.children.append(f) 57 | 58 | self.files[self.root.qid.path] = self.root 59 | for x in self.root.children: 60 | self.files[x.qid.path] = x 61 | 62 | def open(self, srv, req): 63 | '''If we have a file tree then simply check whether the Qid matches 64 | anything inside. respond qid and iounit are set by protocol''' 65 | if req.fid.qid.path not in self.files: 66 | srv.respond(req, "unknown file") 67 | f = self.files[req.fid.qid.path] 68 | if (req.ifcall.mode & f.mode) != py9p.OREAD : 69 | raise py9p.ServerError("permission denied") 70 | srv.respond(req, None) 71 | 72 | def walk(self, srv, req): 73 | # root walks are handled inside the protocol if we have self.root 74 | # set, so don't do them here. '..' however is handled by us, 75 | # trivially 76 | 77 | f = self.files[req.fid.qid.path] 78 | if len(req.ifcall.wname) > 1: 79 | srv.respond(req, "don't know how to handle multiple walks yet") 80 | return 81 | 82 | if req.ifcall.wname[0] == '..': 83 | req.ofcall.wqid.append(f.parent.qid) 84 | srv.respond(req, None) 85 | return 86 | 87 | for x in f.children: 88 | if req.ifcall.wname[0] == x.name: 89 | req.ofcall.wqid.append(x.qid) 90 | srv.respond(req, None) 91 | return 92 | 93 | srv.respond(req, "can't find %s"%req.ifcall.wname[0]) 94 | return 95 | 96 | def stat(self, srv, req): 97 | if req.fid.qid.path not in self.files: 98 | raise py9p.ServerError("unknown file") 99 | req.ofcall.stat.append(self.files[req.fid.qid.path]) 100 | srv.respond(req, None) 101 | 102 | def read(self, srv, req): 103 | if req.fid.qid.path not in self.files: 104 | raise py9p.ServerError("unknown file") 105 | 106 | f = self.files[req.fid.qid.path] 107 | if f.qid.type & py9p.QTDIR: 108 | req.ofcall.stat = [] 109 | for x in f.children: 110 | req.ofcall.stat.append(x) 111 | elif f.name == 'sample1': 112 | if req.ifcall.offset == 0: 113 | buf = 'test\n' 114 | req.ofcall.data = buf[:req.ifcall.count] 115 | else: 116 | req.ofcall.data = '' 117 | elif f.name == 'sample2' : 118 | buf = 'The time is now %s. thank you for asking.\n' % time.asctime(time.localtime(time.time())) 119 | if req.ifcall.offset > len(buf): 120 | req.ofcall.data = '' 121 | else: 122 | req.ofcall.data = buf[req.ifcall.offset : req.ifcall.offset + req.ifcall.count] 123 | 124 | srv.respond(req, None) 125 | 126 | def usage(argv0): 127 | print("usage: %s [-dD] [-p port] [-l listen] [-a authmode] [srvuser domain]" % argv0) 128 | sys.exit(1) 129 | 130 | def main(prog, *args): 131 | listen = 'localhost' 132 | port = py9p.PORT 133 | mods = [] 134 | noauth = 0 135 | dbg = False 136 | user = None 137 | dom = None 138 | passwd = None 139 | authmode = None 140 | key = None 141 | dotu = 0 142 | 143 | try: 144 | opt,args = getopt.getopt(args, "dDp:l:a:") 145 | except Exception as msg: 146 | usage(prog) 147 | for opt,optarg in opt: 148 | if opt == '-d': 149 | dotu = optarg 150 | if opt == "-D": 151 | dbg = True 152 | if opt == "-p": 153 | port = int(optarg) 154 | if opt == '-l': 155 | listen = optarg 156 | if opt == '-a': 157 | authmode = optarg 158 | 159 | if authmode == 'sk1': 160 | if len(args) != 2: 161 | print('missing user and authsrv') 162 | usage(prog) 163 | else: 164 | py9p.sk1 = __import__("py9p.sk1").sk1 165 | user = args[0] 166 | dom = args[1] 167 | passwd = getpass.getpass() 168 | key = py9p.sk1.makeKey(passwd) 169 | elif authmode == 'pki': 170 | py9p.pki = __import__("py9p.pki").pki 171 | user = 'admin' 172 | elif authmode != None and authmode != 'none': 173 | print("unknown auth type: %s; accepted: pki or sk1"%authmode) 174 | sys.exit(1) 175 | 176 | srv = py9p.Server(listen=(listen, port), authmode=authmode, user=user, dom=dom, key=key, chatty=dbg) 177 | srv.mount(SampleFs()) 178 | srv.serve() 179 | 180 | 181 | if __name__ == "__main__" : 182 | try : 183 | main(*sys.argv) 184 | except KeyboardInterrupt : 185 | print("interrupted.") 186 | -------------------------------------------------------------------------------- /examples/composite.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import time 3 | import sys 4 | import getopt 5 | import os 6 | import copy 7 | from py9p import py9p 8 | 9 | import getopt 10 | import getpass 11 | 12 | class SampleFs(py9p.Server): 13 | """ 14 | A sample plugin filesystem. 15 | """ 16 | mountpoint = '/' 17 | root = None 18 | 19 | # files database 20 | files = {} 21 | 22 | def __init__(self): 23 | self.start = int(time.time()) 24 | rootdir = py9p.Dir(0) # not dotu 25 | rootdir.children = [] 26 | rootdir.type = 0 27 | rootdir.dev = 0 28 | rootdir.mode = 020000000755 29 | rootdir.atime = rootdir.mtime = int(time.time()) 30 | rootdir.length = 0 31 | rootdir.name = '/' 32 | rootdir.uid = rootdir.gid = rootdir.muid = os.environ['USER'] 33 | rootdir.qid = py9p.Qid(py9p.QTDIR, 0, py9p.hash8(rootdir.name)) 34 | rootdir.parent = rootdir 35 | self.root = rootdir # / is its own parent, just so we don't fall off the edge of the earth 36 | self.files[self.root.qid.path] = self.root 37 | 38 | def create(self, srv, req): 39 | # get parent 40 | f = self.files[req.fid.qid.path] 41 | if req.ifcall.perm & py9p.DMDIR: 42 | new = py9p.Dir(0, 43 | dev=0, 44 | type=0, 45 | mode=req.ifcall.perm, 46 | length=0, 47 | name=req.ifcall.name, 48 | qid=py9p.Qid(py9p.QTDIR, 0, py9p.hash8(req.ifcall.name)), 49 | uid=os.environ['USER'], 50 | gid=f.gid, 51 | muid=os.environ['USER'], 52 | parent=f) 53 | new.atime = new.mtime = int(time.time()) 54 | new.children = [] 55 | self.files[new.qid.path] = new 56 | f.children.append(new) 57 | req.ofcall.qid = new.qid 58 | else: 59 | new = py9p.Dir(0, 60 | dev=0, 61 | type=0, 62 | mode=req.ifcall.perm, 63 | length=0, 64 | name=req.ifcall.name, 65 | qid=py9p.Qid(0, 0, py9p.hash8(req.ifcall.name)), 66 | uid=os.environ['USER'], 67 | gid=f.gid, 68 | muid=os.environ['USER'], 69 | parent=f) 70 | new.atime = new.mtime = int(time.time()) 71 | self.files[new.qid.path] = new 72 | f.children.append(new) 73 | req.ofcall.qid = new.qid 74 | srv.respond(req, None) 75 | 76 | def open(self, srv, req): 77 | '''If we have a file tree then simply check whether the Qid matches 78 | anything inside. respond qid and iounit are set by protocol''' 79 | if not self.files.has_key(req.fid.qid.path): 80 | srv.respond(req, "unknown file") 81 | f = self.files[req.fid.qid.path] 82 | if (req.ifcall.mode & f.mode) != py9p.OREAD : 83 | raise py9p.ServerError("permission denied") 84 | srv.respond(req, None) 85 | 86 | def walk(self, srv, req): 87 | # root walks are handled inside the protocol if we have self.root 88 | # set, so don't do them here. '..' however is handled by us, 89 | # trivially 90 | 91 | f = self.files[req.fid.qid.path] 92 | if len(req.ifcall.wname) > 1: 93 | srv.respond(req, "don't know how to handle multiple walks yet") 94 | return 95 | 96 | if req.ifcall.wname[0] == '..': 97 | req.ofcall.wqid.append(f.parent.qid) 98 | srv.respond(req, None) 99 | return 100 | 101 | for x in f.children: 102 | if req.ifcall.wname[0] == x.name: 103 | req.ofcall.wqid.append(x.qid) 104 | srv.respond(req, None) 105 | return 106 | 107 | srv.respond(req, "not found") 108 | return 109 | 110 | def stat(self, srv, req): 111 | if not self.files.has_key(req.fid.qid.path): 112 | raise py9p.ServerError("unknown file") 113 | req.ofcall.stat.append(self.files[req.fid.qid.path]) 114 | srv.respond(req, None) 115 | 116 | def read(self, srv, req): 117 | if not self.files.has_key(req.fid.qid.path): 118 | raise py9p.ServerError("unknown file") 119 | 120 | f = self.files[req.fid.qid.path] 121 | if f.qid.type & py9p.QTDIR: 122 | req.ofcall.stat = [] 123 | for x in f.children: 124 | req.ofcall.stat.append(x) 125 | else: 126 | req.ofcall.data = '' 127 | 128 | srv.respond(req, None) 129 | 130 | def usage(argv0): 131 | print "usage: %s [-dD] [-p port] [-l listen] [-a authmode] [srvuser domain]" % argv0 132 | sys.exit(1) 133 | 134 | def main(prog, *args): 135 | 136 | # import rpdb2 137 | # rpdb2.start_embedded_debugger("bala") 138 | listen = 'localhost' 139 | port = py9p.PORT 140 | mods = [] 141 | noauth = 0 142 | dbg = False 143 | user = None 144 | dom = None 145 | passwd = None 146 | authmode = None 147 | key = None 148 | dotu = 0 149 | 150 | try: 151 | opt,args = getopt.getopt(args, "dDp:l:a:") 152 | except Exception, msg: 153 | usage(prog) 154 | for opt,optarg in opt: 155 | if opt == '-d': 156 | dotu = optarg 157 | if opt == "-D": 158 | dbg = True 159 | if opt == "-p": 160 | port = int(optarg) 161 | if opt == '-l': 162 | listen = optarg 163 | if opt == '-a': 164 | authmode = optarg 165 | 166 | if authmode == 'sk1': 167 | if len(args) != 2: 168 | print >>sys.stderr, 'missing user and authsrv' 169 | usage(prog) 170 | else: 171 | py9p.sk1 = __import__("py9p.sk1").sk1 172 | user = args[0] 173 | dom = args[1] 174 | passwd = getpass.getpass() 175 | key = py9p.sk1.makeKey(passwd) 176 | elif authmode == 'pki': 177 | py9p.pki = __import__("py9p.pki").pki 178 | user = 'admin' 179 | elif authmode != None and authmode != 'none': 180 | print >>sys.stderr, "unknown auth type: %s; accepted: pki or sk1"%authmode 181 | sys.exit(1) 182 | 183 | srv = py9p.Server(listen=(listen, port), authmode=authmode, user=user, dom=dom, key=key, chatty=dbg) 184 | srv.mount(SampleFs()) 185 | srv.serve() 186 | 187 | 188 | if __name__ == "__main__" : 189 | try : 190 | main(*sys.argv) 191 | except KeyboardInterrupt : 192 | print "interrupted." 193 | -------------------------------------------------------------------------------- /examples/cl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import socket 3 | import sys 4 | import os 5 | import getopt 6 | import getpass 7 | import code 8 | import readline 9 | import atexit 10 | import fnmatch 11 | 12 | from py9p import py9p 13 | 14 | class Error(py9p.Error): pass 15 | 16 | def _os(func, *args): 17 | try: 18 | return func(*args) 19 | except OSError as e: 20 | raise Error(e.args[1]) 21 | except IOError as e: 22 | raise Error(e.args[1]) 23 | 24 | class HistoryConsole(code.InteractiveConsole): 25 | def __init__(self, locals=None, filename="", 26 | histfile=os.path.expanduser("~/.py9phist")): 27 | code.InteractiveConsole.__init__(self) 28 | self.init_history(histfile) 29 | 30 | def init_history(self, histfile): 31 | readline.parse_and_bind("tab: complete") 32 | if hasattr(readline, "read_history_file"): 33 | try: 34 | readline.read_history_file(histfile) 35 | except IOError: 36 | pass 37 | atexit.register(self.save_history, histfile) 38 | 39 | def save_history(self, histfile): 40 | readline.write_history_file(histfile) 41 | 42 | 43 | class CmdClient(py9p.Client): 44 | def mkdir(self, pstr, perm=0o755): 45 | self.create(pstr, perm|py9p.DMDIR) 46 | self.close() 47 | 48 | def cat(self, name, out=None): 49 | if out is None: 50 | out = sys.stdout 51 | if self.open(name) is None: 52 | return 53 | while 1: 54 | buf = self.read(self.msize) 55 | if len(buf) == 0: 56 | break 57 | out.write(buf.decode('utf-8')) 58 | self.close() 59 | 60 | def put(self, name, inf=None): 61 | if inf is None: 62 | inf = sys.stdin 63 | try: 64 | self.open(name, py9p.OWRITE|py9p.OTRUNC) 65 | except: 66 | self.create(name) 67 | 68 | sz = self.msize 69 | while 1: 70 | buf = inf.read(sz) 71 | self.write(buf) 72 | if len(buf) < sz: 73 | break 74 | self.close() 75 | 76 | def _cmdwrite(self, args): 77 | if len(args) < 1: 78 | print("write: no file name") 79 | elif len(args) == 1: 80 | buf = '' 81 | else: 82 | buf = ' '.join(args[1:]) 83 | 84 | name = args[0] 85 | x = self.open(name, py9p.OWRITE|py9p.OTRUNC) 86 | if x is None: 87 | return 88 | if buf != None: 89 | self.write(buf) 90 | self.close() 91 | 92 | def _cmdecho(self, args): 93 | if len(args) < 1: 94 | print("echo: no file name") 95 | elif len(args) == 1: 96 | buf = '' 97 | else: 98 | buf = ' '.join(args[1:]) 99 | 100 | if buf[-1] != '\n': 101 | buf = buf+'\n' 102 | 103 | name = args[0] 104 | x = self.open(name, py9p.OWRITE|py9p.OTRUNC) 105 | if x is None: 106 | return 107 | self.write(buf) 108 | self.close() 109 | 110 | def _cmdstat(self, args): 111 | for a in args: 112 | stat = self.stat(a) 113 | print(stat[0].tolstr()) 114 | 115 | def _cmdls(self, args): 116 | long = 0 117 | if len(args) > 0 and args[0] == '-l': 118 | long = 1 119 | args[0:1] = [] 120 | ret = self.ls(long, args) 121 | if ret: 122 | if long: 123 | print('\n'.join(ret)) 124 | else: 125 | print(' '.join(ret)) 126 | 127 | def _cmdcd(self, args): 128 | if len(args) != 1: 129 | print("usage: cd path") 130 | return 131 | if self.cd(args[0]): 132 | if args[0][0] == '/': 133 | self.path = os.path.normpath(args[0]) 134 | else: 135 | self.path = os.path.normpath(self.path + "/" + args[0]) 136 | 137 | 138 | def _cmdio(self, args): 139 | if len(args) != 1: 140 | print("usage: io path") 141 | return 142 | self.io(args[0]) 143 | 144 | def _cmdcat(self, args): 145 | if len(args) != 1: 146 | print("usage: cat path") 147 | return 148 | self.cat(args[0]) 149 | 150 | def _cmdmkdir(self, args): 151 | if len(args) != 1: 152 | print("usage: mkdir path") 153 | return 154 | self.mkdir(args[0]) 155 | def _cmdget(self, args): 156 | if len(args) == 1: 157 | f, = args 158 | f2 = f.split("/")[-1] 159 | elif len(args) == 2: 160 | f,f2 = args 161 | else: 162 | print("usage: get path [localname]") 163 | return 164 | out = _os(file, f2, "wb") 165 | self.cat(f, out) 166 | out.close() 167 | def _cmdput(self, args): 168 | if len(args) == 1: 169 | f, = args 170 | f2 = f.split("/")[-1] 171 | elif len(args) == 2: 172 | f,f2 = args 173 | else: 174 | print("usage: put path [remotename]") 175 | return 176 | if f == '-': 177 | inf = sys.stdin 178 | else: 179 | inf = _os(file, f, "rb") 180 | self.put(f2, inf) 181 | if f != '-': 182 | inf.close() 183 | def _cmdpwd(self, args): 184 | if len(args) == 0: 185 | print(os.path.normpath(self.path)) 186 | else: 187 | print("usage: pwd") 188 | def _cmdrm(self, args): 189 | if len(args) == 1: 190 | self.rm(args[0]) 191 | else: 192 | print("usage: rm path") 193 | def _cmdhelp(self, args): 194 | cmds = [x[4:] for x in dir(self) if x[:4] == "_cmd"] 195 | cmds.sort() 196 | print("commands: ", " ".join(cmds)) 197 | def _cmdquit(self, args): 198 | self.done = 1 199 | _cmdexit = _cmdquit 200 | 201 | def _nextline(self): # generator is cleaner but not supported in 2.2 202 | if self.cmds is None: 203 | #sys.stdout.write("9p> ") 204 | #sys.stdout.flush() 205 | #line = sys.stdin.readline() 206 | line = self.cons.raw_input("9p> ") 207 | if line != "": 208 | return line 209 | else: 210 | if self.cmds: 211 | x,self.cmds = self.cmds[0],self.cmds[1:] 212 | return x 213 | 214 | def completer(self, text, state): 215 | ret = None 216 | cmds = [x[4:] for x in dir(self) if x[:4] == "_cmd"] 217 | cmds.sort() 218 | 219 | line = readline.get_line_buffer() 220 | level = line.split() 221 | if (len(level) == 0) or (len(level) == 1 and line[-1] != ' '): 222 | # match commands 223 | if text == '' and state < len(cmds): 224 | ret = cmds[state] 225 | else: 226 | l = filter(lambda x: x.startswith(text), cmds) 227 | if len(l) > state: 228 | ret = l[state]+' ' 229 | elif len(level) == 2 or line[-1] == ' ': 230 | # match files 231 | if state == 0: 232 | self.lsfiles = self.ls() 233 | self.lsfiles.sort() 234 | ls = self.lsfiles 235 | if text == '' and state < len(cmds): 236 | ret = ls[state] 237 | else: 238 | l = filter(lambda x: x.startswith(text), ls) 239 | if len(l) > state: 240 | ret = l[state]+' ' 241 | return ret 242 | 243 | def cmdLoop(self, cmds): 244 | self.cons = HistoryConsole() 245 | cmdf = {} 246 | for n in dir(self): 247 | if n[:4] == "_cmd": 248 | cmdf[n[4:]] = getattr(self, n) 249 | 250 | self.done = 0 251 | if not cmds: 252 | cmds = None 253 | else: 254 | self.done = 1 # exit after running the commands 255 | self.cmds = cmds 256 | while 1: 257 | line = self._nextline() 258 | if line is None: 259 | continue 260 | args = list(filter(None, line.split(" "))) 261 | if not args: 262 | continue 263 | cmd,args = args[0],args[1:] 264 | if cmd in cmdf: 265 | try: 266 | cmdf[cmd](args) 267 | except py9p.Error as e: 268 | print("%s error: %s" % (cmd, e.args[0])) 269 | if e.args[0] == 'client eof': 270 | break 271 | else: 272 | sys.stdout.write("%s ?\n" % cmd) 273 | if self.done and not self.cmds: 274 | break 275 | 276 | def usage(prog): 277 | print("usage: %s [-d] [-m authmode] [-a authsrv] [-k privkey] [user@]srv[:port] [cmd ...]" % prog) 278 | sys.exit(1) 279 | 280 | def main(): 281 | prog = sys.argv[0] 282 | args = sys.argv[1:] 283 | port = py9p.PORT 284 | authsrv = None 285 | chatty = 0 286 | authmode = 'none' 287 | privkey = None 288 | 289 | user = os.environ.get('USER') 290 | try: 291 | opt,args = getopt.getopt(args, "da:u:p:m:k:") 292 | except: 293 | usage(prog) 294 | passwd = None 295 | 296 | for opt,optarg in opt: 297 | if opt == '-m': 298 | authmode = optarg 299 | if opt == '-a': 300 | authsrv = optarg 301 | if opt == '-d': 302 | chatty = 1 303 | if opt == "-p": 304 | port = int(optarg) # XXX catch 305 | if opt == '-u': 306 | user = optarg 307 | if opt == '-k': 308 | privkey = optarg 309 | 310 | if len(args) < 1: 311 | print("error: no server to connect to...") 312 | usage(prog) 313 | 314 | srvkey = args[0].split('@', 1) 315 | if len(srvkey) == 2: 316 | user = srvkey[0] 317 | srvkey = srvkey[1] 318 | else: 319 | srvkey = srvkey[0] 320 | 321 | srvkey = srvkey.split(':', 1) 322 | if len(srvkey) == 2: 323 | port = int(srvkey[1]) 324 | srvkey = srvkey[0] 325 | 326 | srv = srvkey 327 | if chatty: 328 | print("connecting as %s to %s, port %d" % (user, srv, port)) 329 | 330 | if authmode == 'sk1' and authsrv is None: 331 | print("assuming %s is also auth server" % srv) 332 | authsrv = srv 333 | 334 | cmd = args[1:] 335 | 336 | sock = socket.socket(socket.AF_INET) 337 | try: 338 | sock.connect((srv, port),) 339 | except socket.error as e: 340 | print("%s: %s" % (srv, e.args[1])) 341 | return 342 | 343 | if authmode == 'sk1' and passwd is None: 344 | passwd = getpass.getpass() 345 | try: 346 | creds = py9p.Credentials(user, authmode, passwd, privkey) 347 | cl = CmdClient(sock, creds, authsrv, chatty) 348 | readline.set_completer(cl.completer) 349 | cl.cmdLoop(cmd) 350 | except py9p.Error as e: 351 | print(e) 352 | 353 | #''' 354 | if __name__ == "__main__": 355 | try: 356 | main() 357 | except KeyboardInterrupt: 358 | print("interrupted.") 359 | except EOFError: 360 | print("done.") 361 | except Exception as m: 362 | print("unhandled exception: " + str(m.args)) 363 | raise 364 | ''' 365 | if __name__ == "__main__": 366 | import trace 367 | 368 | # create a Trace object, telling it what to ignore, and whether to 369 | # do tracing or line-counting or both. 370 | tracer = trace.Trace( 371 | ignoredirs=[sys.prefix, sys.exec_prefix], 372 | trace=1, 373 | count=1) 374 | 375 | # run the new command using the given tracer 376 | tracer.run('main()') 377 | # make a report, placing output in /tmp 378 | r = tracer.results() 379 | r.write_results(show_missing=True, coverdir="/tmp") 380 | #''' 381 | -------------------------------------------------------------------------------- /9pfs/9pfs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2008-2011 Tim Newsham, Andrey Mirtchovski 4 | # Copyright (c) 2011-2012 Peter V. Saveliev 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining 7 | # a copy of this software and associated documentation files (the 8 | # "Software"), to deal in the Software without restriction, including 9 | # without limitation the rights to use, copy, modify, merge, publish, 10 | # distribute, sublicense, and/or sell copies of the Software, and to 11 | # permit persons to whom the Software is furnished to do so, subject to 12 | # the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be 15 | # included in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | import sys 26 | import stat 27 | import os.path 28 | import pwd 29 | import grp 30 | import getopt 31 | import getpass 32 | 33 | from py9p import py9p 34 | 35 | 36 | def _os(func, *args): 37 | try: 38 | return func(*args) 39 | except OSError as e: 40 | raise py9p.ServerError(e.args) 41 | except IOError as e: 42 | raise py9p.ServerError(e.args) 43 | 44 | 45 | def _nf(func, *args): 46 | try: 47 | return func(*args) 48 | except py9p.ServerError: 49 | return 50 | 51 | 52 | def uidname(u): 53 | try: 54 | return "%s" % pwd.getpwuid(u).pw_name 55 | except KeyError: 56 | return "%d" % u 57 | 58 | 59 | def gidname(g): 60 | try: 61 | return "%s" % grp.getgrgid(g).gr_name 62 | except KeyError: 63 | return "%d" % g 64 | 65 | 66 | class LocalFs(object): 67 | """ 68 | A local filesystem device. 69 | """ 70 | 71 | files = {} 72 | 73 | def __init__(self, root, cancreate=0, dotu=0): 74 | self.dotu = dotu 75 | self.cancreate = cancreate 76 | self.root = self.pathtodir(root) 77 | self.root.parent = self.root 78 | self.root.localpath = root 79 | self.files[self.root.qid.path] = self.root 80 | 81 | def getfile(self, path): 82 | if path not in self.files: 83 | return None 84 | return self.files[path] 85 | 86 | def pathtodir(self, f): 87 | '''Stat-to-dir conversion''' 88 | s = _os(os.lstat, f) 89 | u = uidname(s.st_uid) 90 | g = gidname(s.st_gid) 91 | res = s.st_mode & 0o777 92 | type = 0 93 | ext = "" 94 | if stat.S_ISDIR(s.st_mode): 95 | type = type | py9p.QTDIR 96 | res = res | py9p.DMDIR 97 | qid = py9p.Qid(type, 0, py9p.hash8(f)) 98 | if self.dotu: 99 | if stat.S_ISLNK(s.st_mode): 100 | res = py9p.DMSYMLINK 101 | ext = os.readlink(f) 102 | elif stat.S_ISCHR(s.st_mode): 103 | ext = "c %d %d" % (os.major(s.st_rdev), os.minor(s.st_rdev)) 104 | elif stat.S_ISBLK(s.st_mode): 105 | ext = "b %d %d" % (os.major(s.st_rdev), os.minor(s.st_rdev)) 106 | else: 107 | ext = "" 108 | 109 | return py9p.Dir(1, 0, s.st_dev, qid, 110 | res, 111 | int(s.st_atime), int(s.st_mtime), 112 | s.st_size, os.path.basename(f), u, gidname(s.st_gid), u, 113 | ext, s.st_uid, s.st_gid, s.st_uid) 114 | else: 115 | return py9p.Dir(0, 0, s.st_dev, qid, 116 | res, 117 | int(s.st_atime), int(s.st_mtime), 118 | s.st_size, os.path.basename(f), u, g, u) 119 | 120 | def open(self, srv, req): 121 | f = self.getfile(req.fid.qid.path) 122 | s = _os(os.lstat, f.localpath) 123 | if not f: 124 | srv.respond(req, "unknown file") 125 | return 126 | if (req.ifcall.mode & 3) == py9p.OWRITE: 127 | if not self.cancreate: 128 | srv.respond(req, "read-only file server") 129 | return 130 | if req.ifcall.mode & py9p.OTRUNC: 131 | m = "wb" 132 | else: 133 | m = "r+b" # almost 134 | elif (req.ifcall.mode & 3) == py9p.ORDWR: 135 | if not self.cancreate: 136 | srv.respond(req, "read-only file server") 137 | return 138 | if m & py9p.OTRUNC: 139 | m = "w+b" 140 | else: 141 | m = "r+b" 142 | else: # py9p.OREAD and otherwise 143 | m = "rb" 144 | if not (f.qid.type & py9p.QTDIR) and not stat.S_ISLNK(s.st_mode): 145 | f.fd = _os(open, f.localpath, m) 146 | srv.respond(req, None) 147 | 148 | def walk(self, srv, req): 149 | f = self.getfile(req.fid.qid.path) 150 | if not f: 151 | srv.respond(req, 'unknown file') 152 | return 153 | npath = f.localpath 154 | for path in req.ifcall.wname: 155 | # normpath takes care to remove '.' and '..', turn '//' into '/' 156 | npath = os.path.normpath(npath + "/" + path) 157 | if len(npath) <= len(self.root.localpath): 158 | # don't let us go beyond the original root 159 | npath = self.root.localpath 160 | 161 | if path == '.' or path == '': 162 | req.ofcall.wqid.append(f.qid) 163 | elif path == '..': 164 | # .. resolves to the parent, cycles at / 165 | qid = f.parent.qid 166 | req.ofcall.wqid.append(qid) 167 | f = f.parent 168 | else: 169 | try: 170 | d = self.pathtodir(npath) 171 | except: 172 | srv.respond(req, "file not found") 173 | return 174 | 175 | nf = self.getfile(d.qid.path) 176 | if nf: 177 | # already exists, just append to req 178 | req.ofcall.wqid.append(d.qid) 179 | f = nf 180 | else: 181 | d.localpath = npath 182 | d.basedir = "/".join(npath.split("/")[:-1]) 183 | d.parent = f 184 | self.files[d.qid.path] = d 185 | req.ofcall.wqid.append(d.qid) 186 | f = d 187 | 188 | req.ofcall.nwqid = len(req.ofcall.wqid) 189 | srv.respond(req, None) 190 | 191 | def remove(self, srv, req): 192 | f = self.getfile(req.fid.qid.path) 193 | if not f: 194 | srv.respond(req, 'unknown file') 195 | return 196 | if not self.cancreate: 197 | srv.respond(req, "read-only file server") 198 | return 199 | 200 | if f.qid.type & py9p.QTDIR: 201 | _os(os.rmdir, f.localpath) 202 | else: 203 | _os(os.remove, f.localpath) 204 | self.files[req.fid.qid.path] = None 205 | srv.respond(req, None) 206 | 207 | def create(self, srv, req): 208 | fd = None 209 | if not self.cancreate: 210 | srv.respond(req, "read-only file server") 211 | return 212 | if req.ifcall.name == '.' or req.ifcall.name == '..': 213 | srv.respond(req, "illegal file name") 214 | return 215 | 216 | f = self.getfile(req.fid.qid.path) 217 | if not f: 218 | srv.respond(req, 'unknown file') 219 | return 220 | name = f.localpath + '/' + req.ifcall.name 221 | if req.ifcall.perm & py9p.DMDIR: 222 | perm = req.ifcall.perm & (~0o777 | (f.mode & 0o777)) 223 | _os(os.mkdir, name, req.ifcall.perm & ~(py9p.DMDIR)) 224 | elif req.ifcall.perm & py9p.DMSYMLINK and self.dotu: 225 | _os(os.symlink, req.ifcall.extension, name) 226 | else: 227 | perm = req.ifcall.perm & (~0o666 | (f.mode & 0o666)) 228 | _os(open, name, "w+").close() 229 | _os(os.chmod, name, perm) 230 | if (req.ifcall.mode & 3) == py9p.OWRITE: 231 | if req.ifcall.mode & py9p.OTRUNC: 232 | m = "wb" 233 | else: 234 | m = "r+b" # almost 235 | elif (req.ifcall.mode & 3) == py9p.ORDWR: 236 | if m & py9p.OTRUNC: 237 | m = "w+b" 238 | else: 239 | m = "r+b" 240 | else: # py9p.OREAD and otherwise 241 | m = "rb" 242 | fd = _os(open, name, m) 243 | 244 | d = self.pathtodir(name) 245 | d.parent = f 246 | self.files[d.qid.path] = d 247 | self.files[d.qid.path].localpath = name 248 | self.files[d.qid.path].basedir = "/".join(name.split("/")[:-1]) 249 | if fd: 250 | self.files[d.qid.path].fd = fd 251 | req.ofcall.qid = d.qid 252 | srv.respond(req, None) 253 | 254 | def clunk(self, srv, req): 255 | f = self.getfile(req.fid.qid.path) 256 | if not f: 257 | srv.respond(req, 'unknown file') 258 | return 259 | f = self.files[req.fid.qid.path] 260 | if hasattr(f, 'fd') and f.fd is not None: 261 | f.fd.close() 262 | f.fd = None 263 | srv.respond(req, None) 264 | 265 | def stat(self, srv, req): 266 | f = self.getfile(req.fid.qid.path) 267 | if not f: 268 | srv.respond(req, "unknown file") 269 | return 270 | req.ofcall.stat.append(self.pathtodir(f.localpath)) 271 | srv.respond(req, None) 272 | 273 | def wstat(self, srv, req): 274 | 275 | istat = req.ifcall.stat[0] 276 | f = self.getfile(req.fid.qid.path) 277 | if (istat.uidnum >> 16) == 0xFFFF: 278 | istat.uidnum = -1 279 | if (istat.gidnum >> 16) == 0xFFFF: 280 | istat.gidnum = -1 281 | 282 | _os(os.chown, f.localpath, istat.uidnum, istat.gidnum) 283 | # change mode? 284 | if istat.mode != 0xFFFFFFFF: 285 | s = _os(os.lstat, f.localpath) 286 | imode = s.st_mode 287 | mode = ((imode & 0o7777) ^ imode) |\ 288 | (istat.mode & 0o7777) 289 | _os(os.chmod, f.localpath, mode) 290 | # change name? 291 | if istat.name: 292 | _os(os.rename, f.localpath, "/".join((f.basedir, 293 | istat.name.decode('utf-8')))) 294 | srv.respond(req, None) 295 | 296 | def read(self, srv, req): 297 | f = self.getfile(req.fid.qid.path) 298 | s = _os(os.lstat, f.localpath) 299 | if not f: 300 | srv.respond(req, "unknown file") 301 | return 302 | 303 | if stat.S_ISLNK(s.st_mode) and self.dotu: 304 | d = self.pathtodir(f.localpath) 305 | req.ofcall.data = d.extension 306 | elif f.qid.type & py9p.QTDIR: 307 | # no need to add anything to self.files yet 308 | # wait until they walk to it 309 | l = _os(os.listdir, f.localpath) 310 | l = filter(lambda x: x not in ('.', '..'), l) 311 | req.ofcall.stat = [] 312 | for x in l: 313 | req.ofcall.stat.append(self.pathtodir(f.localpath + '/' + x)) 314 | else: 315 | f.fd.seek(req.ifcall.offset) 316 | req.ofcall.data = f.fd.read(req.ifcall.count) 317 | srv.respond(req, None) 318 | 319 | def write(self, srv, req): 320 | if not self.cancreate: 321 | srv.respond(req, "read-only file server") 322 | return 323 | 324 | f = self.getfile(req.fid.qid.path) 325 | if not f: 326 | srv.respond(req, "unknown file") 327 | return 328 | 329 | f.fd.seek(req.ifcall.offset) 330 | f.fd.write(req.ifcall.data) 331 | req.ofcall.count = len(req.ifcall.data) 332 | srv.respond(req, None) 333 | 334 | 335 | def usage(prog): 336 | print("usage: %s [-dDw] [-c mode] [-p port] [-r root] " \ 337 | "[-a address] [srvuser [domain]]" % prog) 338 | sys.exit(1) 339 | 340 | 341 | def main(): 342 | prog = sys.argv[0] 343 | args = sys.argv[1:] 344 | 345 | port = py9p.PORT 346 | listen = '0.0.0.0' 347 | root = '/' 348 | user = None 349 | chatty = 0 350 | cancreate = 0 351 | dotu = 0 352 | authmode = None 353 | dom = None 354 | passwd = None 355 | key = None 356 | 357 | try: 358 | opt, args = getopt.getopt(args, "dDwp:r:a:c:") 359 | except: 360 | usage(prog) 361 | for opt, optarg in opt: 362 | if opt == "-d": 363 | chatty = 1 364 | if opt == "-D": 365 | dotu = 1 366 | if opt == '-w': 367 | cancreate = 1 368 | if opt == '-r': 369 | root = optarg 370 | if opt == "-p": 371 | port = int(optarg) 372 | if opt == '-a': 373 | listen = optarg 374 | if opt == '-c': 375 | authmode = optarg 376 | 377 | if authmode == 'pki': 378 | try: 379 | py9p.pki = __import__("py9p.pki").pki 380 | user = 'admin' 381 | except: 382 | import traceback 383 | traceback.print_exc() 384 | elif authmode is not None and authmode != 'none': 385 | print("unknown auth type: %s; accepted: pki, sk1, none" % authmode) 386 | sys.exit(1) 387 | 388 | srv = py9p.Server(listen=(listen, port), 389 | authmode=authmode, 390 | user=user, 391 | dom=dom, 392 | key=key, 393 | chatty=chatty, 394 | dotu=dotu) 395 | srv.mount(LocalFs(root, cancreate, dotu)) 396 | srv.serve() 397 | 398 | if __name__ == "__main__": 399 | try: 400 | main() 401 | except KeyboardInterrupt: 402 | print("interrupted.") 403 | -------------------------------------------------------------------------------- /py9p/pki.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2008-2011 Tim Newsham, Andrey Mirtchovski 2 | # Copyright (c) 2011-2012 Peter V. Saveliev 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining 5 | # a copy of this software and associated documentation files (the 6 | # "Software"), to deal in the Software without restriction, including 7 | # without limitation the rights to use, copy, modify, merge, publish, 8 | # distribute, sublicense, and/or sell copies of the Software, and to 9 | # permit persons to whom the Software is furnished to do so, subject to 10 | # the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | """ 24 | Implementation of basic RSA-key digital signature. 25 | 26 | Description: 27 | - Client sends server an Auth message to establish an auth fid. 28 | - Server prepares reads client's public key and generates a random MD5 key 29 | for the signature encrypting it with the key. 30 | - Client decrypts the hash with its public key, signs it, encrypts the 31 | signature and sends it to Server 32 | - Server verifies the signature and allows an 'attach' message from client 33 | 34 | Public keys are, for now, taken from client's ~/.ssh/id_rsa.pub 35 | 36 | This module requires the Python Cryptography Toolkit from 37 | http://www.amk.ca/python/writing/pycrypt/pycrypt.html 38 | """ 39 | 40 | import base64 41 | import struct 42 | import os 43 | import random 44 | import getpass 45 | import pickle 46 | import Crypto.Util as util 47 | import hashlib 48 | import sys 49 | from . import utils as c9 50 | from Crypto.Cipher import DES3, AES 51 | from Crypto.PublicKey import RSA, DSA 52 | from Crypto.Util.randpool import RandomPool 53 | from Crypto.Util import number 54 | from Crypto.Hash import MD5 55 | from binascii import unhexlify 56 | 57 | 58 | class Error(Exception): 59 | pass 60 | 61 | 62 | class AuthError(Error): 63 | pass 64 | 65 | 66 | class AuthsrvError(Error): 67 | pass 68 | 69 | 70 | class BadKeyError(Error): 71 | pass 72 | 73 | 74 | class BadKeyPassword(Error): 75 | pass 76 | 77 | 78 | class ServerError(Error): 79 | pass 80 | 81 | 82 | def gethome(uname): 83 | for x in open('/etc/passwd').readlines(): 84 | u = x.split(':') 85 | if uname == u[0]: 86 | return u[5] 87 | 88 | 89 | def asn1parse(data): 90 | things = [] 91 | while data: 92 | t = ord(data[0]) 93 | assert (t & 0xc0) == 0, 'not a universal value: 0x%02x' % t 94 | #assert t & 0x20, 'not a constructed value: 0x%02x' % t 95 | l = ord(data[1]) 96 | assert data != 0x80, "shouldn't be an indefinite length" 97 | if l & 0x80: # long form 98 | ll = l & 0x7f 99 | l = number.bytes_to_long(data[2:2 + ll]) 100 | s = 2 + ll 101 | else: 102 | s = 2 103 | body, data = data[s:s + l], data[s + l:] 104 | t = t & (~0x20) 105 | assert t in (SEQUENCE, INTEGER), 'bad type: 0x%02x' % t 106 | if t == SEQUENCE: 107 | things.append(asn1parse(body)) 108 | elif t == INTEGER: 109 | #assert (ord(body[0])&0x80) == 0, "shouldn't have negative number" 110 | things.append(number.bytes_to_long(body)) 111 | if len(things) == 1: 112 | return things[0] 113 | return things 114 | 115 | 116 | def asn1pack(data): 117 | ret = '' 118 | for part in data: 119 | if type(part) in (tuple, list): 120 | partData = asn1pack(part) 121 | partType = SEQUENCE | 0x20 122 | elif type(part) in (int, long): 123 | partData = number.long_to_bytes(part) 124 | if ord(partData[0]) & (0x80): 125 | partData = '\x00' + partData 126 | partType = INTEGER 127 | else: 128 | raise 'unknown type %s' % type(part) 129 | 130 | ret += chr(partType) 131 | if len(partData) > 127: 132 | l = number.long_to_bytes(len(partData)) 133 | ret += chr(len(l) | 0x80) + l 134 | else: 135 | ret += chr(len(partData)) 136 | ret += partData 137 | return ret 138 | 139 | INTEGER = 0x02 140 | SEQUENCE = 0x10 141 | 142 | Length = 1024 143 | 144 | 145 | def NS(t): 146 | return struct.pack('!L', len(t)) + t 147 | 148 | 149 | def getNS(s, count=1): 150 | ns = [] 151 | c = 0 152 | for i in range(count): 153 | l, = struct.unpack('!L', s[c:c + 4]) 154 | ns.append(s[c + 4:4 + l + c]) 155 | c += 4 + l 156 | return tuple(ns) + (s[c:],) 157 | 158 | 159 | def MP(number): 160 | if number == 0: 161 | return '\000' * 4 162 | assert number > 0 163 | bn = util.number.long_to_bytes(number) 164 | if ord(bn[0]) & 128: 165 | bn = '\000' + bn 166 | return struct.pack('>L', len(bn)) + bn 167 | 168 | 169 | def getMP(data): 170 | """ 171 | get multiple precision integer 172 | """ 173 | length = struct.unpack('>L', data[:4])[0] 174 | return util.number.bytes_to_long(data[4:4 + length]), data[4 + length:] 175 | 176 | 177 | def privkeytostr(key, passphrase=None): 178 | keyData = '-----BEGIN RSA PRIVATE KEY-----\n' 179 | p, q = key.p, key.q 180 | if p > q: 181 | (p, q) = (q, p) 182 | # p is less than q 183 | objData = [0, key.n, key.e, key.d, q, p, key.d % (q - 1), 184 | key.d % (p - 1), util.number.inverse(p, q)] 185 | if passphrase: 186 | iv = RandomPool().get_bytes(8) 187 | hexiv = ''.join(['%02X' % ord(x) for x in iv]) 188 | keyData += 'Proc-Type: 4,ENCRYPTED\n' 189 | keyData += 'DEK-Info: DES-EDE3-CBC,%s\n\n' % hexiv 190 | ba = hashlib.md5(passphrase + iv).digest() 191 | bb = hashlib.md5(ba + passphrase + iv).digest() 192 | encKey = (ba + bb)[:24] 193 | asn1Data = asn1pack([objData]) 194 | if passphrase: 195 | padLen = 8 - (len(asn1Data) % 8) 196 | asn1Data += (chr(padLen) * padLen) 197 | asn1Data = DES3.new(encKey, DES3.MODE_CBC, iv).encrypt(asn1Data) 198 | b64Data = base64.encodestring(asn1Data).replace('\n', '') 199 | b64Data = '\n'.join([b64Data[i:i + 64] for i in 200 | range(0, len(b64Data), 64)]) 201 | keyData += b64Data + '\n' 202 | keyData += '-----END RSA PRIVATE KEY-----' 203 | return keyData 204 | 205 | 206 | def pubkeytostr(key, comment=None): 207 | keyData = MP(key.e) + MP(key.n) 208 | b64Data = base64.encodestring(NS("ssh-rsa") + keyData).replace('\n', '') 209 | return '%s %s %s' % ("ssh-rsa", b64Data, comment) 210 | 211 | 212 | def strtopubkey(data): 213 | d = base64.decodestring(data.split(b' ')[1]) 214 | kind, rest = getNS(d) 215 | if kind == b'ssh-rsa': 216 | e, rest = getMP(rest) 217 | n, rest = getMP(rest) 218 | return RSA.construct((n, e)) 219 | else: 220 | raise Exception('unknown key type %s' % kind) 221 | 222 | 223 | def get_key_data(salt, password, keysize): 224 | keydata = '' 225 | digest = '' 226 | # truncate salt 227 | salt = salt[:8] 228 | while keysize > 0: 229 | hash_obj = MD5.new() 230 | if len(digest) > 0: 231 | hash_obj.update(digest) 232 | hash_obj.update(password) 233 | hash_obj.update(salt) 234 | digest = hash_obj.digest() 235 | size = min(keysize, len(digest)) 236 | keydata += digest[:size] 237 | keysize -= size 238 | return keydata 239 | 240 | 241 | def strtoprivkey(data, password): 242 | kind = data[0][11: 14] 243 | if data[1].startswith('Proc-Type: 4,ENCRYPTED'): # encrypted key 244 | if not password: 245 | raise BadKeyPassword("password required") 246 | enc_type, salt = data[2].split(": ")[1].split(",") 247 | salt = unhexlify(salt.strip()) 248 | b64Data = base64.decodestring(''.join(data[4:-1])) 249 | if enc_type == "DES-EDE3-CBC": 250 | key = get_key_data(salt, password, 24) 251 | keyData = DES3.new(key, DES3.MODE_CBC, salt).decrypt(b64Data) 252 | elif enc_type == "AES-128-CBC": 253 | key = get_key_data(salt, password, 16) 254 | keyData = AES.new(key, AES.MODE_CBC, salt).decrypt(b64Data) 255 | else: 256 | raise BadKeyError("unknown encryption") 257 | removeLen = ord(keyData[-1]) 258 | keyData = keyData[:-removeLen] 259 | else: 260 | keyData = base64.decodestring(''.join(data[1:-1])) 261 | decodedKey = asn1parse(keyData) 262 | if isinstance(decodedKey[0], list): 263 | decodedKey = decodedKey[0] # this happens with encrypted keys 264 | if kind == 'RSA': 265 | n, e, d, p, q = decodedKey[1:6] 266 | return RSA.construct((n, e, d, p, q)) 267 | elif kind == 'DSA': 268 | p, q, g, y, x = decodedKey[1: 6] 269 | return DSA.construct((y, g, p, q, x)) 270 | 271 | 272 | def getprivkey(uname, priv=None, passphrase=None): 273 | if not uname: 274 | raise AuthError("no uname") 275 | 276 | if priv is None: 277 | f = gethome(uname) 278 | if not f: 279 | raise BadKeyError("no home dir for user %s" % uname) 280 | f += '/.ssh/id_rsa' 281 | if not os.path.exists(f): 282 | raise BadKeyError("no private key and no " + f) 283 | else: 284 | privkey = file(f).readlines() 285 | elif not os.path.exists(priv): 286 | raise BadKeyError("file not found: " + priv) 287 | else: 288 | privkey = file(priv).readlines() 289 | 290 | try: 291 | return strtoprivkey(privkey, passphrase) 292 | except BadKeyPassword: 293 | passphrase = getpass.getpass("password: ") 294 | return strtoprivkey(privkey, passphrase) 295 | 296 | 297 | def getchallenge(): 298 | # generate a 16-byte long random string. (note that the built- 299 | # in pseudo-random generator uses a 24-bit seed, so this is not 300 | # as good as it may seem...) 301 | challenge = map(lambda i: c9.bytes3(chr(random.randint(0x20, 0x7e))), range(16)) 302 | return b''.join(challenge) 303 | 304 | 305 | class AuthFs(object): 306 | """ 307 | A special file for performing our pki authentication variant. 308 | On completion of the protocol, suid is set to the authenticated 309 | username. 310 | """ 311 | type = 'pki' 312 | HaveChal, NeedSign, Success = range(3) 313 | cancreate = 0 314 | pubkeys = {} 315 | 316 | def __init__(self, keys=None): 317 | self.keyfiles = keys or {} 318 | self.pubkeys = {} 319 | 320 | def addpubkeyfromfile(self, uname, pub): 321 | pubkey = file(pub).read() 322 | self.pubkeys[uname] = strtopubkey(pubkey) 323 | 324 | def addpubkey(self, uname, pub): 325 | self.pubkeys[uname] = strtopubkey(pub) 326 | 327 | def delpubkey(self, uname): 328 | if uname in self.pubkeys: 329 | del self.pubkeys[uname] 330 | else: 331 | raise BadKeyError("no key for %s" % uname) 332 | 333 | def getpubkey(self, uname, pub=None): 334 | if not uname: 335 | raise AuthError('no uname') 336 | if uname in self.pubkeys: 337 | return self.pubkeys[uname] 338 | elif pub is None: 339 | f = gethome(uname) 340 | if not f: 341 | raise BadKeyError("no home for user %s" % uname) 342 | f += '/.ssh/id_rsa.pub' 343 | if not os.path.exists(f): 344 | raise BadKeyError("no public key supplied and no " + f) 345 | else: 346 | pubkey = open(f, 'rb').read() 347 | elif not os.path.exists(pub): 348 | raise BadKeyError("file not found: " + pub) 349 | else: 350 | pubkey = open(pub, 'rb').read() 351 | 352 | self.pubkeys[uname] = strtopubkey(pubkey) 353 | return self.pubkeys[uname] 354 | 355 | def estab(self, fid): 356 | fid.suid = None 357 | fid.phase = self.HaveChal 358 | if not hasattr(fid, 'uname'): 359 | raise AuthError("no fid.uname") 360 | uname = fid.uname.decode('utf-8') 361 | fid.key = self.getpubkey(uname, 362 | self.keyfiles.get(uname, None)) 363 | fid.chal = getchallenge() 364 | 365 | def read(self, srv, req): 366 | f = req.fid 367 | if f.phase == self.HaveChal: 368 | f.phase = self.NeedSign 369 | req.ofcall.data = pickle.dumps(f.key.encrypt(f.chal, ''), protocol=2) 370 | srv.respond(req, None) 371 | return 372 | elif f.phase == self.Success: 373 | req.ofcall.data = 'success as ' + f.suid 374 | srv.respond(req, None) 375 | return 376 | raise ServerError("unexpected phase") 377 | 378 | def write(self, srv, req): 379 | f = req.fid 380 | buf = req.ifcall.data 381 | if f.phase == self.NeedSign: 382 | signature = pickle.loads(buf) 383 | if f.key.verify(f.chal, signature): 384 | f.phase = self.Success 385 | f.suid = f.uname 386 | req.ofcall.count = len(buf) 387 | srv.respond(req, None) 388 | return 389 | else: 390 | raise ServerError('signature not verified') 391 | raise ServerError("unexpected phase") 392 | 393 | 394 | def clientAuth(cl, fcall, credentials): 395 | pos = [0] 396 | 397 | def rd(l): 398 | fc = cl._read(fcall.afid, pos[0], l) 399 | pos[0] += len(fc.data) 400 | return fc.data 401 | 402 | def wr(x): 403 | fc = cl._write(fcall.afid, pos[0], x) 404 | pos[0] += fc.count 405 | return fc.count 406 | 407 | c = pickle.loads(rd(2048)) 408 | chal = credentials.key.decrypt(c) 409 | sign = credentials.key.sign(chal, '') 410 | 411 | wr(pickle.dumps(sign, protocol=2)) 412 | return 413 | -------------------------------------------------------------------------------- /py9p/fuse9p.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011-2012 Peter V. Saveliev 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining 4 | # a copy of this software and associated documentation files (the 5 | # "Software"), to deal in the Software without restriction, including 6 | # without limitation the rights to use, copy, modify, merge, publish, 7 | # distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so, subject to 9 | # the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be 12 | # included in all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | import socket 23 | import sys 24 | import os 25 | import pwd 26 | import grp 27 | import fuse 28 | import stat 29 | import errno 30 | import time 31 | import threading 32 | import py9p 33 | import traceback 34 | 35 | MIN_TFID = 64 36 | MAX_TFID = 1023 37 | MIN_FID = 1024 38 | MAX_FID = 65535 39 | MAX_RECONNECT_INTERVAL = 1024 40 | IOUNIT = 1024 * 16 41 | FAIL_TRIES = 2 42 | FAIL_TIMEOUT = 0.5 43 | 44 | uid_map = {} 45 | gid_map = {} 46 | rpccodes = { 47 | py9p.Eunknownfid: -errno.EBADFD, 48 | py9p.Edupfid: -errno.EBADFD, 49 | py9p.Ebaddir: -errno.EBADFD, 50 | py9p.Enocreate: -errno.EPERM, 51 | py9p.Enoremove: -errno.EPERM, 52 | py9p.Enostat: -errno.EPERM, 53 | py9p.Enowstat: -errno.EPERM, 54 | py9p.Eperm: -errno.EPERM, 55 | py9p.Enotfound: -errno.ENOENT, 56 | py9p.Eisdir: -errno.EISDIR, 57 | py9p.Ewalknotdir: -errno.ENOTDIR, 58 | py9p.Ecreatenondir: -errno.ENOTDIR} 59 | 60 | 61 | class Error(py9p.Error): 62 | pass 63 | 64 | 65 | class NoFidError(Exception): 66 | pass 67 | 68 | 69 | fuse.fuse_python_api = (0, 2) 70 | 71 | 72 | class fStat(fuse.Stat): 73 | """ 74 | FUSE stat structure, that will represent PyVFS Inode 75 | """ 76 | def __init__(self, inode): 77 | self.st_mode = py9p.mode2stat(inode.mode) 78 | self.st_ino = 0 79 | self.st_dev = 0 80 | if inode.mode & stat.S_IFDIR: 81 | self.st_nlink = inode.length 82 | else: 83 | self.st_nlink = 1 84 | self.st_uid = int(uid_map.get(inode.uidnum, inode.uidnum)) 85 | self.st_gid = int(gid_map.get(inode.gidnum, inode.gidnum)) 86 | self.st_size = inode.length 87 | self.st_atime = inode.atime 88 | self.st_mtime = inode.mtime 89 | self.st_ctime = inode.mtime 90 | 91 | 92 | class fakeRoot(fuse.Stat): 93 | """ 94 | Fake empty root for disconnected state 95 | """ 96 | def __init__(self): 97 | self.st_mode = stat.S_IFDIR | 0o755 98 | self.st_ino = 0 99 | self.st_dev = 0 100 | self.st_nlink = 3 101 | self.st_uid = 0 102 | self.st_gid = 0 103 | self.st_size = 3 104 | self.st_atime = self.st_mtime = self.st_ctime = time.time() 105 | 106 | 107 | def guard(c): 108 | """ 109 | The decorator function, specific for ClientFS class 110 | 111 | * acqiures and releases temporary fid 112 | * deals with py9p RPC errors 113 | * triggers reconnect() on network errors 114 | """ 115 | def wrapped(self, *argv, **kwarg): 116 | ret = -errno.EIO 117 | for i in range(FAIL_TRIES): 118 | try: 119 | tfid = self.tfidcache.acquire() 120 | with self._rlock: 121 | ret = c(self, tfid.fid, *argv, **kwarg) 122 | self.tfidcache.release(tfid) 123 | break 124 | except NoFidError: 125 | ret = -errno.EMFILE 126 | break 127 | except py9p.RpcError as e: 128 | ret = rpccodes.get(e.message.lower(), -errno.EIO) 129 | break 130 | except: 131 | if self.debug: 132 | traceback.print_exc() 133 | if self.keep_reconnect: 134 | self._reconnect() 135 | time.sleep(FAIL_TIMEOUT) 136 | else: 137 | sys.exit(255) 138 | return ret 139 | return wrapped 140 | 141 | 142 | class FidCache(dict): 143 | """ 144 | Fid cache class 145 | 146 | The class provides API to acquire next not used Fid 147 | for the 9p operations. If there is no free Fid available, 148 | it raises NoFidError(). After usage, Fid should be freed 149 | and returned to the cache with release() method. 150 | """ 151 | def __init__(self, start=MIN_FID, limit=MAX_FID): 152 | """ 153 | * start -- the Fid interval beginning 154 | * limit -- the Fid interval end 155 | 156 | All acquired Fids will be from this interval. 157 | """ 158 | dict.__init__(self) 159 | self.start = start 160 | self.limit = limit 161 | self.iounit = IOUNIT 162 | self.fids = list(range(self.start, self.limit + 1)) 163 | 164 | def acquire(self): 165 | """ 166 | Acquire next available Fid 167 | """ 168 | if len(self.fids) < 1: 169 | raise NoFidError() 170 | return Fid(self.fids.pop(0), self.iounit) 171 | 172 | def release(self, f): 173 | """ 174 | Return Fid to the free Fids queue. 175 | """ 176 | self.fids.append(f.fid) 177 | 178 | 179 | class Fid(object): 180 | """ 181 | Fid class 182 | 183 | It is used also in the stateful I/O, representing 184 | the open file. All methods, working with open files, 185 | will receive Fid as the last parameter. 186 | 187 | See: write(), read(), release() 188 | """ 189 | def __init__(self, fid, iounit=IOUNIT): 190 | self.fid = fid 191 | self.iounit = iounit 192 | 193 | 194 | class ClientFS(fuse.Fuse): 195 | """ 196 | FUSE subclass 197 | 198 | Implements all the proxying of FUSE calls to 9p 199 | server. Can authomatically reconnect to the server. 200 | """ 201 | def __init__(self, address, credentials, mountpoint, 202 | debug=False, timeout=10, keep_reconnect=False): 203 | """ 204 | * address -- (address,port) of the 9p server, tuple 205 | * credentials -- py9p.Credentials 206 | * mountpoint -- where to mount the FS 207 | * debug -- FUSE and py9p debug output, implies foreground run 208 | * timeout -- socket timeout 209 | * keep_reconnect -- whether to try reconnect after errors 210 | """ 211 | 212 | self.address = address 213 | self.credentials = credentials 214 | self.debug = debug 215 | self.timeout = timeout 216 | self.msize = IOUNIT 217 | self.sock = None 218 | self.exit = None 219 | self.dotu = 1 220 | self.keep_reconnect = keep_reconnect 221 | self._lock = threading.Lock() 222 | self._rlock = threading.RLock() 223 | self._interval = 1 224 | self._reconnect_event = threading.Event() 225 | self._connected_event = threading.Event() 226 | self.fidcache = FidCache() 227 | self._reconnect(init=True) 228 | self.dircache = {} 229 | self.tfidcache = FidCache(start=MIN_TFID, limit=MAX_TFID) 230 | 231 | fuse.Fuse.__init__(self, version="%prog " + fuse.__version__, 232 | dash_s_do='undef') 233 | 234 | if debug: 235 | self.fuse_args.setmod('foreground') 236 | self.fuse_args.add('debug') 237 | self.fuse_args.add('large_read') 238 | self.fuse_args.add('big_writes') 239 | self.fuse_args.mountpoint = os.path.realpath(mountpoint) 240 | 241 | def fsinit(self): 242 | # daemon mode RNG hack for PyCrypto 243 | try: 244 | from Crypto import Random 245 | Random.atfork() 246 | except: 247 | pass 248 | 249 | def _reconnect(self, init=False, dotu=1): 250 | """ 251 | Start reconnection thread. When init=True, just probe 252 | the connection and return even if keep_reconnect=True. 253 | """ 254 | if self._lock.acquire(False): 255 | self._connected_event.clear() 256 | t = threading.Thread( 257 | target=self._reconnect_target, 258 | args=(init, dotu)) 259 | t.setDaemon(True) 260 | t.start() 261 | if init: 262 | # in the init state we MUST NOT leave 263 | # any thread; all running threads will be 264 | # suspended by FUSE in the "daemon" 265 | # multithreaded mode 266 | t.join() 267 | else: 268 | # otherwise, just run reconnection 269 | # thread in the background 270 | self._connected_event.wait(self.timeout + 2) 271 | if self.exit: 272 | print(str(self.exit)) 273 | sys.exit(255) 274 | 275 | def _reconnect_interval(self): 276 | """ 277 | Return next reconnection interval in seconds. 278 | """ 279 | self._interval = min(self._interval * 2, MAX_RECONNECT_INTERVAL) 280 | return self._interval 281 | 282 | def _reconnect_target(self, init=False, dotu=1): 283 | """ 284 | Reconnection thread code. 285 | """ 286 | while True: 287 | try: 288 | self.sock.close() 289 | except: 290 | pass 291 | 292 | try: 293 | if self.debug: 294 | print("trying to connect") 295 | if self.address[0].find("/") > -1: 296 | self.sock = socket.socket(socket.AF_UNIX) 297 | else: 298 | self.sock = socket.socket(socket.AF_INET) 299 | self.sock.settimeout(self.timeout) 300 | self.sock.connect(self.address) 301 | self.client = py9p.Client( 302 | fd=self.sock, 303 | chatty=self.debug, 304 | credentials=self.credentials, 305 | dotu=dotu, msize=self.msize) 306 | self.msize = self.client.msize 307 | self.fidcache.iounit = self.client.msize - py9p.IOHDRSZ 308 | self._connected_event.set() 309 | self._lock.release() 310 | return 311 | except py9p.VersionError: 312 | if dotu: 313 | self.dotu = 0 314 | self._reconnect_target(init, 0) 315 | else: 316 | self.exit = Exception("protocol negotiation error") 317 | return 318 | except Exception as e: 319 | if self.keep_reconnect: 320 | if init: 321 | # if we get an error on the very initial 322 | # time, just fake the connection -- 323 | # next reconnect round will be triggered 324 | # by the next failed FS call 325 | self._lock.release() 326 | self._connected_event.set() 327 | return 328 | s = self._reconnect_interval() 329 | if self.debug: 330 | print("reconnect in %s seconds" % (s)) 331 | self._reconnect_event.wait(s) 332 | self._reconnect_event.clear() 333 | else: 334 | self.exit = e 335 | self._lock.release() 336 | self._connected_event.set() 337 | return 338 | 339 | @guard 340 | def open(self, tfid, path, mode): 341 | f = self.fidcache.acquire() 342 | try: 343 | self.client._walk(self.client.ROOT, 344 | f.fid, filter(None, path.split("/"))) 345 | fcall = self.client._open(f.fid, py9p.open2plan(mode)) 346 | f.iounit = fcall.iounit 347 | return f 348 | except Exception as e: 349 | self.fidcache.release(f) 350 | raise e 351 | 352 | @guard 353 | def _wstat(self, tfid, path, 354 | uid=py9p.ERRUNDEF, 355 | gid=py9p.ERRUNDEF, 356 | mode=py9p.ERRUNDEF, 357 | newname=None): 358 | self.client._walk(self.client.ROOT, 359 | tfid, filter(None, path.split("/"))) 360 | if self.dotu: 361 | stats = [py9p.Dir( 362 | dotu=1, 363 | type=0, 364 | dev=0, 365 | qid=py9p.Qid(0, 0, py9p.hash8(path)), 366 | mode=mode, 367 | atime=int(time.time()), 368 | mtime=int(time.time()), 369 | length=py9p.ERRUNDEF, 370 | name=newname or path.split("/")[-1], 371 | uid="", 372 | gid="", 373 | muid="", 374 | extension="", 375 | uidnum=uid, 376 | gidnum=gid, 377 | muidnum=py9p.ERRUNDEF), ] 378 | else: 379 | stats = [py9p.Dir( 380 | dotu=0, 381 | type=0, 382 | dev=0, 383 | qid=py9p.Qid(0, 0, py9p.hash8(path)), 384 | mode=mode, 385 | atime=int(time.time()), 386 | mtime=int(time.time()), 387 | length=py9p.ERRUNDEF, 388 | name=newname or path.split("/")[-1], 389 | uid=pwd.getpwuid(uid).pw_name, 390 | gid=grp.getgrgid(gid).gr_name, 391 | muid=""), ] 392 | self.client._wstat(tfid, stats) 393 | self.client._clunk(tfid) 394 | 395 | def chmod(self, path, mode): 396 | return self._wstat(path, mode=py9p.mode2plan(mode)) 397 | 398 | def chown(self, path, uid, gid): 399 | return self._wstat(path, uid, gid) 400 | 401 | def utime(self, path, times): 402 | pass 403 | 404 | @guard 405 | def unlink(self, tfid, path): 406 | self.client._walk(self.client.ROOT, 407 | tfid, filter(None, path.split("/"))) 408 | self.client._remove(tfid) 409 | self.dircache = {} 410 | 411 | def rmdir(self, path): 412 | self.unlink(path) 413 | 414 | @guard 415 | def symlink(self, tfid, target, path): 416 | if not self.dotu: 417 | return -errno.ENOSYS 418 | self.client._walk(self.client.ROOT, tfid, 419 | filter(None, path.split("/"))[:-1]) 420 | self.client._create(tfid, filter(None, path.split("/"))[-1], 421 | py9p.DMSYMLINK, 0, target) 422 | self.client._clunk(tfid) 423 | 424 | @guard 425 | def mknod(self, tfid, path, mode, dev): 426 | if dev != 0: 427 | return -errno.ENOSYS 428 | # FIXME 429 | if not mode & stat.S_IFREG: 430 | mode |= stat.S_IFDIR 431 | try: 432 | self.client._walk(self.client.ROOT, 433 | tfid, filter(None, path.split("/"))) 434 | self.client._open(tfid, py9p.OTRUNC) 435 | self.client._clunk(tfid) 436 | except py9p.RpcError as e: 437 | if e.message == "file not found": 438 | self.client._walk(self.client.ROOT, 439 | tfid, filter(None, path.split("/"))[:-1]) 440 | self.client._create(tfid, 441 | filter(None, path.split("/"))[-1], 442 | py9p.mode2plan(mode), 0) 443 | self.client._clunk(tfid) 444 | else: 445 | return -errno.EIO 446 | 447 | def mkdir(self, path, mode): 448 | return self.mknod(path, mode | stat.S_IFDIR, 0) 449 | 450 | @guard 451 | def truncate(self, tfid, path, size): 452 | if size != 0: 453 | return -errno.ENOSYS 454 | self.client._walk(self.client.ROOT, 455 | tfid, filter(None, path.split("/"))) 456 | self.client._open(tfid, py9p.OTRUNC) 457 | self.client._clunk(tfid) 458 | 459 | @guard 460 | def write(self, tfid, path, buf, offset, f): 461 | if py9p.hash8(path) in self.dircache: 462 | del self.dircache[py9p.hash8(path)] 463 | size = len(buf) 464 | for i in range((size + f.iounit - 1) / f.iounit): 465 | start = i * f.iounit 466 | length = start + f.iounit 467 | self.client._write(f.fid, offset + start, 468 | buf[start:length]) 469 | return size 470 | 471 | @guard 472 | def read(self, tfid, path, size, offset, f): 473 | data = bytes() 474 | i = 0 475 | while True: 476 | # we do not rely nor on msize, neither on iounit, 477 | # so, shift offset only with real data read 478 | ret = self.client._read(f.fid, offset, 479 | min(size - len(data), f.iounit)) 480 | data += ret.data 481 | offset += len(ret.data) 482 | if size <= len(data) or len(ret.data) == 0: 483 | break 484 | i += 1 485 | return data[:size] 486 | 487 | @guard 488 | def rename(self, tfid, path, dest): 489 | # the most complicated routine :| 490 | # 9p protocol has no "rename" neither "move" call 491 | # in the meaning of Linux vfs, it can only change 492 | # the name of an entry w/o moving it from dir to 493 | # dir, which can be done with wstat() 494 | 495 | for i in (path, dest): 496 | if py9p.hash8(i) in self.dircache: 497 | del self.dircache[py9p.hash8(i)] 498 | 499 | # if we can use wstat(): 500 | if path.split("/")[:-1] == dest.split("/")[:-1]: 501 | return self._wstat(path, newname=dest.split("/")[-1]) 502 | 503 | # it is not simple rename, fall back to copy/delete: 504 | # 505 | # get source and destination 506 | source = self._getattr(path) 507 | destination = self._getattr(dest) 508 | # abort on EIO 509 | if -errno.EIO in (source, destination): 510 | return -errno.EIO 511 | # create the destination file 512 | if destination == -errno.ENOENT: 513 | self.mknod(dest, source.st_mode, 0) 514 | if source.st_mode & stat.S_IFDIR: 515 | # move all the content to the new directory 516 | for i in self._readdir(path, 0): 517 | self.rename( 518 | "/".join((path, i.name)), 519 | "/".join((dest, i.name))) 520 | else: 521 | # open both files 522 | sf = self.open(path, os.O_RDONLY) 523 | df = self.open(dest, os.O_WRONLY | os.O_TRUNC) 524 | # copy the content 525 | for i in range((source.st_size + self.msize - 1) / self.msize): 526 | block = self.read(path, self.msize, i * self.msize, sf) 527 | self.write(dest, block, i * self.msize, df) 528 | # close files 529 | self.release(path, 0, sf) 530 | self.release(dest, 0, df) 531 | # remove the source 532 | self.unlink(path) 533 | 534 | @guard 535 | def release(self, tfid, path, flags, f): 536 | try: 537 | self.client._clunk(f.fid) 538 | self.fidcache.release(f) 539 | except: 540 | pass 541 | 542 | @guard 543 | def readlink(self, tfid, path): 544 | if py9p.hash8(path) in self.dircache: 545 | return self.dircache[py9p.hash8(path)].extension 546 | self.client._walk(self.client.ROOT, 547 | tfid, filter(None, path.split("/"))) 548 | self.client._open(tfid, py9p.OREAD) 549 | ret = self.client._read(tfid, 0, self.msize) 550 | self.client._clunk(tfid) 551 | return ret.data 552 | 553 | @guard 554 | def _getattr(self, tfid, path): 555 | if py9p.hash8(path) in self.dircache: 556 | return fStat(self.dircache[py9p.hash8(path)]) 557 | 558 | self.client._walk(self.client.ROOT, 559 | tfid, filter(None, path.split("/"))) 560 | ret = self.client._stat(tfid).stat[0] 561 | 562 | s = fStat(ret) 563 | self.client._clunk(tfid) 564 | self.dircache[py9p.hash8(path)] = ret 565 | return s 566 | 567 | def getattr(self, path): 568 | self._interval = 1 569 | self._reconnect_event.set() 570 | 571 | s = self._getattr(path) 572 | 573 | if s == -errno.EIO: 574 | if self.keep_reconnect: 575 | if path == "/": 576 | return fakeRoot() 577 | else: 578 | return -errno.ENOENT 579 | return s 580 | 581 | @guard 582 | def _readdir(self, tfid, path, offset): 583 | dirs = [] 584 | self.client._walk(self.client.ROOT, 585 | tfid, filter(None, path.split("/"))) 586 | self.client._open(tfid, py9p.OREAD) 587 | offset = 0 588 | while True: 589 | ret = self.client._read(tfid, offset, self.msize) 590 | if len(ret.data) == 0: 591 | break 592 | offset += len(ret.data) 593 | p9 = py9p.Marshal9P(dotu=self.dotu) 594 | p9.setBuffer(ret.data) 595 | p9.buf.seek(0) 596 | fcall = py9p.Fcall(py9p.Rstat) 597 | p9.decstat(fcall.stat, 0) 598 | dirs.extend(fcall.stat) 599 | self.client._clunk(tfid) 600 | return dirs 601 | 602 | def readdir(self, path, offset): 603 | self._interval = 1 604 | self._reconnect_event.set() 605 | 606 | dirs = self._readdir(path, offset) 607 | if not isinstance(dirs, list): 608 | dirs = [] 609 | 610 | if path == "/": 611 | path = "" 612 | for i in dirs: 613 | self.dircache[py9p.hash8("/".join((path, i.name)))] = i 614 | yield fuse.Direntry(i.name) 615 | -------------------------------------------------------------------------------- /py9p/py9p.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2008-2011 Tim Newsham, Andrey Mirtchovski 2 | # Copyright (c) 2011-2012 Peter V. Saveliev 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining 5 | # a copy of this software and associated documentation files (the 6 | # "Software"), to deal in the Software without restriction, including 7 | # without limitation the rights to use, copy, modify, merge, publish, 8 | # distribute, sublicense, and/or sell copies of the Software, and to 9 | # permit persons to whom the Software is furnished to do so, subject to 10 | # the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | """ 24 | 9P protocol implementation as documented in plan9 intro(5) and . 25 | """ 26 | 27 | import os 28 | import stat 29 | import sys 30 | import socket 31 | import select 32 | import traceback 33 | import io 34 | import threading 35 | import struct 36 | from . import utils as c9 37 | 38 | if sys.version_info[0] == 3: 39 | unicode = str 40 | 41 | 42 | IOHDRSZ = 24 43 | PORT = 564 44 | 45 | cmdName = {} 46 | 47 | 48 | Tversion = 100 49 | Rversion = 101 50 | Tauth = 102 51 | Rauth = 103 52 | Tattach = 104 53 | Rattach = 105 54 | Terror = 106 55 | Rerror = 107 56 | Tflush = 108 57 | Rflush = 109 58 | Twalk = 110 59 | Rwalk = 111 60 | Topen = 112 61 | Ropen = 113 62 | Tcreate = 114 63 | Rcreate = 115 64 | Tread = 116 65 | Rread = 117 66 | Twrite = 118 67 | Rwrite = 119 68 | Tclunk = 120 69 | Rclunk = 121 70 | Tremove = 122 71 | Rremove = 123 72 | Tstat = 124 73 | Rstat = 125 74 | Twstat = 126 75 | Rwstat = 127 76 | 77 | for i, k in dict(globals()).items(): 78 | try: 79 | if (i[0] in ('T', 'R')) and isinstance(k, int): 80 | cmdName[k] = i 81 | except: 82 | pass 83 | 84 | version = b'9P2000' 85 | versionu = b'9P2000.u' 86 | 87 | Ebadoffset = "bad offset" 88 | Ebotch = "9P protocol botch" 89 | Ecreatenondir = "create in non-directory" 90 | Edupfid = "duplicate fid" 91 | Eduptag = "duplicate tag" 92 | Eisdir = "is a directory" 93 | Enocreate = "create prohibited" 94 | Enoremove = "remove prohibited" 95 | Enostat = "stat prohibited" 96 | Enotfound = "file not found" 97 | Enowstat = "wstat prohibited" 98 | Eperm = "permission denied" 99 | Eunknownfid = "unknown fid" 100 | Ebaddir = "bad directory in wstat" 101 | Ewalknotdir = "walk in non-directory" 102 | Eopen = "file not open" 103 | 104 | NOTAG = 0xffff 105 | NOFID = 0xffffffff 106 | 107 | # for completeness including all of p9p's defines 108 | OREAD = 0 # open for read 109 | OWRITE = 1 # write 110 | ORDWR = 2 # read and write 111 | OEXEC = 3 # execute, == read but check execute permission 112 | OTRUNC = 16 # or'ed in (except for exec), truncate file first 113 | OCEXEC = 32 # or'ed in, close on exec 114 | ORCLOSE = 64 # or'ed in, remove on close 115 | ODIRECT = 128 # or'ed in, direct access 116 | ONONBLOCK = 256 # or'ed in, non-blocking call 117 | OEXCL = 0x1000 # or'ed in, exclusive use (create only) 118 | OLOCK = 0x2000 # or'ed in, lock after opening 119 | OAPPEND = 0x4000 # or'ed in, append only 120 | 121 | AEXIST = 0 # accessible: exists 122 | AEXEC = 1 # execute access 123 | AWRITE = 2 # write access 124 | AREAD = 4 # read access 125 | 126 | # Qid.type 127 | QTDIR = 0x80 # type bit for directories 128 | QTAPPEND = 0x40 # type bit for append only files 129 | QTEXCL = 0x20 # type bit for exclusive use files 130 | QTMOUNT = 0x10 # type bit for mounted channel 131 | QTAUTH = 0x08 # type bit for authentication file 132 | QTTMP = 0x04 # type bit for non-backed-up file 133 | QTSYMLINK = 0x02 # type bit for symbolic link 134 | QTFILE = 0x00 # type bits for plain file 135 | 136 | # Dir.mode 137 | DMDIR = 0x80000000 # mode bit for directories 138 | DMAPPEND = 0x40000000 # mode bit for append only files 139 | DMEXCL = 0x20000000 # mode bit for exclusive use files 140 | DMMOUNT = 0x10000000 # mode bit for mounted channel 141 | DMAUTH = 0x08000000 # mode bit for authentication file 142 | DMTMP = 0x04000000 # mode bit for non-backed-up file 143 | DMSYMLINK = 0x02000000 # mode bit for symbolic link (Unix, 9P2000.u) 144 | DMDEVICE = 0x00800000 # mode bit for device file (Unix, 9P2000.u) 145 | DMNAMEDPIPE = 0x00200000 # mode bit for named pipe (Unix, 9P2000.u) 146 | DMSOCKET = 0x00100000 # mode bit for socket (Unix, 9P2000.u) 147 | DMSETUID = 0x00080000 # mode bit for setuid (Unix, 9P2000.u) 148 | DMSETGID = 0x00040000 # mode bit for setgid (Unix, 9P2000.u) 149 | DMSTICKY = 0x00010000 # mode bit for sticky bit (Unix, 9P2000.u) 150 | 151 | DMREAD = 0x4 # mode bit for read permission 152 | DMWRITE = 0x2 # mode bit for write permission 153 | DMEXEC = 0x1 # mode bit for execute permission 154 | 155 | ERRUNDEF = 0xFFFFFFFF 156 | UIDUNDEF = 0xFFFFFFFF 157 | 158 | # supported authentication protocols 159 | auths = ['pki', 'sk1'] 160 | 161 | 162 | class Error(Exception): 163 | pass 164 | 165 | 166 | class EofError(Error): 167 | pass 168 | 169 | 170 | class EdupfidError(Error): 171 | pass 172 | 173 | 174 | class RpcError(Error): 175 | pass 176 | 177 | 178 | class ServerError(Error): 179 | pass 180 | 181 | 182 | class ClientError(Error): 183 | pass 184 | 185 | 186 | class VersionError(Error): 187 | pass 188 | 189 | 190 | class Marshal9P(object): 191 | chatty = False 192 | 193 | @property 194 | def length(self): 195 | p = self.buf.tell() 196 | self.buf.seek(0, 2) 197 | l = self.buf.tell() 198 | self.buf.seek(p) 199 | return l 200 | 201 | def enc1(self, x): 202 | """Encode 1-byte unsigned""" 203 | self.buf.write(struct.pack('B', x)) 204 | 205 | def dec1(self): 206 | """Decode 1-byte unsigned""" 207 | return struct.unpack('b', self.buf.read(1))[0] 208 | 209 | def enc2(self, x): 210 | """Encode 2-byte unsigned""" 211 | self.buf.write(struct.pack('H', x)) 212 | 213 | def dec2(self): 214 | """Decode 2-byte unsigned""" 215 | return struct.unpack('H', self.buf.read(2))[0] 216 | 217 | def enc4(self, x): 218 | """Encode 4-byte unsigned""" 219 | self.buf.write(struct.pack('I', x)) 220 | 221 | def dec4(self): 222 | """Decode 4-byte unsigned""" 223 | return struct.unpack('I', self.buf.read(4))[0] 224 | 225 | def enc8(self, x): 226 | """Encode 8-byte unsigned""" 227 | self.buf.write(struct.pack('Q', x)) 228 | 229 | def dec8(self): 230 | """Decode 8-byte unsigned""" 231 | return struct.unpack('Q', self.buf.read(8))[0] 232 | 233 | def encS(self, x): 234 | """Encode data string with 2-byte length""" 235 | self.buf.write(struct.pack("H", len(x))) 236 | if isinstance(x, str) or isinstance(x, unicode): 237 | x = c9.bytes3(x) 238 | self.buf.write(x) 239 | 240 | def decS(self): 241 | """Decode data string with 2-byte length""" 242 | return self.buf.read(self.dec2()) 243 | 244 | def encD(self, d): 245 | """Encode data string with 4-byte length""" 246 | self.buf.write(struct.pack("I", len(d))) 247 | if isinstance(d, str) or isinstance(d, unicode): 248 | d = c9.bytes3(d) 249 | self.buf.write(d) 250 | 251 | def decD(self): 252 | """Decode data string with 4-byte length""" 253 | return self.buf.read(self.dec4()) 254 | 255 | def encF(self, *argv): 256 | """Encode data directly by struct.pack""" 257 | self.buf.write(struct.pack(*argv)) 258 | 259 | def decF(self, fmt, length): 260 | """Decode data by struct.unpack""" 261 | return struct.unpack(fmt, self.buf.read(length)) 262 | 263 | def encQ(self, q): 264 | """Encode Qid structure""" 265 | self.encF("=BIQ", q.type, q.vers, q.path) 266 | 267 | def decQ(self): 268 | """Decode Qid structure""" 269 | return Qid(self.dec1(), self.dec4(), self.dec8()) 270 | 271 | def __init__(self, dotu=0, chatty=False): 272 | self.chatty = chatty 273 | self.dotu = dotu 274 | self._lock = threading.Lock() 275 | self.buf = None 276 | 277 | def _checkType(self, t): 278 | if t not in cmdName: 279 | raise Error("Invalid message type %d" % t) 280 | 281 | def _checkSize(self, v, mask): 282 | if v != v & mask: 283 | raise Error("Invalid value %d" % v) 284 | 285 | def _checkLen(self, x, l): 286 | if len(x) != l: 287 | raise Error("Wrong length %d, expected %d: %r" % ( 288 | len(x), l, x)) 289 | 290 | def setBuffer(self, init=b""): 291 | self.buf = io.BytesIO() 292 | self.buf.write(init) 293 | 294 | def send(self, fd, fcall): 295 | "Format and send a message" 296 | with self._lock: 297 | self.setBuffer(b"0000") 298 | self._checkType(fcall.type) 299 | if self.chatty: 300 | print("-%d-> %s %s %s" % (fd.fileno(), cmdName[fcall.type], \ 301 | fcall.tag, fcall.tostr())) 302 | self.enc(fcall) 303 | self.buf.seek(0) 304 | self.enc4(self.length) 305 | fd.write(self.buf.getvalue()) 306 | 307 | def recv(self, fd): 308 | "Read and decode a message" 309 | with self._lock: 310 | size = struct.unpack("I", fd.read(4))[0] 311 | if size > 0xffffffff or size < 7: 312 | raise Error("Bad message size: %d" % size) 313 | self.setBuffer(fd.read(size - 4)) 314 | self.buf.seek(0) 315 | mtype, tag = self.decF("=BH", 3) 316 | self._checkType(mtype) 317 | fcall = Fcall(mtype, tag) 318 | self.dec(fcall) 319 | # self._checkResid() -- FIXME: check the message residue 320 | if self.chatty: 321 | print("<-%d- %s %s %s" % (fd.fileno(), cmdName[mtype], 322 | tag, fcall.tostr())) 323 | return fcall 324 | 325 | def encstat(self, stats, enclen=1): 326 | statsz = 0 327 | for x in stats: 328 | if self.dotu: 329 | x.statsz = 61 + \ 330 | len(x.name) + len(x.uid) + len(x.gid) + \ 331 | len(x.muid) + len(x.extension) 332 | statsz += x.statsz 333 | else: 334 | x.statsz = 47 + \ 335 | len(x.name) + len(x.uid) + len(x.gid) + \ 336 | len(x.muid) 337 | statsz += x.statsz 338 | if enclen: 339 | self.enc2(statsz + 2) 340 | 341 | for x in stats: 342 | self.encF("=HHIBIQIIIQ", 343 | x.statsz, x.type, x.dev, x.qid.type, x.qid.vers, 344 | x.qid.path, x.mode, x.atime, x.mtime, x.length) 345 | self.encS(x.name) 346 | self.encS(x.uid) 347 | self.encS(x.gid) 348 | self.encS(x.muid) 349 | if self.dotu: 350 | self.encS(x.extension) 351 | self.encF("=III", 352 | x.uidnum, x.gidnum, x.muidnum) 353 | 354 | def enc(self, fcall): 355 | self.encF("=BH", fcall.type, fcall.tag) 356 | if fcall.type in (Tversion, Rversion): 357 | self.encF("I", fcall.msize) 358 | self.encS(fcall.version) 359 | elif fcall.type == Tauth: 360 | self.encF("I", fcall.afid) 361 | self.encS(fcall.uname) 362 | self.encS(fcall.aname) 363 | if self.dotu: 364 | self.encF("I", fcall.uidnum) 365 | elif fcall.type == Rauth: 366 | self.encQ(fcall.aqid) 367 | elif fcall.type == Rerror: 368 | self.encS(fcall.ename) 369 | if self.dotu: 370 | self.encF("I", fcall.errno) 371 | elif fcall.type == Tflush: 372 | self.encF("H", fcall.oldtag) 373 | elif fcall.type == Tattach: 374 | self.encF("=II", fcall.fid, fcall.afid) 375 | self.encS(fcall.uname) 376 | self.encS(fcall.aname) 377 | if self.dotu: 378 | self.encF("I", fcall.uidnum) 379 | elif fcall.type == Rattach: 380 | self.encQ(fcall.qid) 381 | elif fcall.type == Twalk: 382 | self.encF("=IIH", fcall.fid, fcall.newfid, 383 | len(fcall.wname)) 384 | for x in fcall.wname: 385 | self.encS(x) 386 | elif fcall.type == Rwalk: 387 | self.encF("H", len(fcall.wqid)) 388 | for x in fcall.wqid: 389 | self.encQ(x) 390 | elif fcall.type == Topen: 391 | self.encF("=IB", fcall.fid, fcall.mode) 392 | elif fcall.type in (Ropen, Rcreate): 393 | self.encQ(fcall.qid) 394 | self.encF("I", fcall.iounit) 395 | elif fcall.type == Tcreate: 396 | self.encF("I", fcall.fid) 397 | self.encS(fcall.name) 398 | self.encF("=IB", fcall.perm, fcall.mode) 399 | if self.dotu: 400 | self.encS(fcall.extension) 401 | elif fcall.type == Tread: 402 | self.encF("=IQI", fcall.fid, fcall.offset, 403 | fcall.count) 404 | elif fcall.type == Rread: 405 | self.encD(fcall.data) 406 | elif fcall.type == Twrite: 407 | self.encF("=IQI", fcall.fid, fcall.offset, 408 | len(fcall.data)) 409 | self.buf.write(fcall.data) 410 | elif fcall.type == Rwrite: 411 | self.encF("I", fcall.count) 412 | elif fcall.type in (Tclunk, Tremove, Tstat): 413 | self.encF("I", fcall.fid) 414 | elif fcall.type in (Rstat, Twstat): 415 | if fcall.type == Twstat: 416 | self.encF("I", fcall.fid) 417 | self.encstat(fcall.stat, 1) 418 | 419 | def decstat(self, stats, enclen=0): 420 | if enclen: 421 | # feed 2 bytes of total size 422 | self.buf.read(2) 423 | while self.buf.tell() < self.length: 424 | self.buf.read(2) 425 | 426 | s = Dir(self.dotu) 427 | (s.type, 428 | s.dev, 429 | typ, vers, path, 430 | s.mode, 431 | s.atime, 432 | s.mtime, 433 | s.length) = self.decF("=HIBIQIIIQ", 39) 434 | s.qid = Qid(typ, vers, path) 435 | s.name = self.decS() # name 436 | s.uid = self.decS() # uid 437 | s.gid = self.decS() # gid 438 | s.muid = self.decS() # muid 439 | if self.dotu: 440 | s.extension = self.decS() 441 | (s.uidnum, 442 | s.gidnum, 443 | s.muidnum) = self.decF("=III", 12) 444 | stats.append(s) 445 | 446 | def dec(self, fcall): 447 | if fcall.type in (Tversion, Rversion): 448 | fcall.msize = self.dec4() 449 | fcall.version = self.decS() 450 | elif fcall.type == Tauth: 451 | fcall.afid = self.dec4() 452 | fcall.uname = self.decS() 453 | fcall.aname = self.decS() 454 | if self.dotu: 455 | fcall.uidnum = self.dec4() 456 | elif fcall.type == Rauth: 457 | fcall.aqid = self.decQ() 458 | elif fcall.type == Rerror: 459 | fcall.ename = self.decS() 460 | if self.dotu: 461 | fcall.errno = self.dec4() 462 | elif fcall.type == Tflush: 463 | fcall.oldtag = self.dec2() 464 | elif fcall.type == Tattach: 465 | fcall.fid = self.dec4() 466 | fcall.afid = self.dec4() 467 | fcall.uname = self.decS() 468 | fcall.aname = self.decS() 469 | if self.dotu: 470 | fcall.uidnum = self.dec4() 471 | elif fcall.type == Rattach: 472 | fcall.qid = self.decQ() 473 | elif fcall.type == Twalk: 474 | fcall.fid = self.dec4() 475 | fcall.newfid = self.dec4() 476 | fcall.nwname = self.dec2() 477 | fcall.wname = [self.decS().decode('utf-8') for n in range(fcall.nwname)] 478 | elif fcall.type == Rwalk: 479 | fcall.nwqid = self.dec2() 480 | fcall.wqid = [self.decQ() for n in range(fcall.nwqid)] 481 | elif fcall.type == Topen: 482 | fcall.fid = self.dec4() 483 | fcall.mode = self.dec1() 484 | elif fcall.type in (Ropen, Rcreate): 485 | fcall.qid = self.decQ() 486 | fcall.iounit = self.dec4() 487 | elif fcall.type == Tcreate: 488 | fcall.fid = self.dec4() 489 | fcall.name = self.decS().decode('utf-8') 490 | fcall.perm = self.dec4() 491 | fcall.mode = self.dec1() 492 | if self.dotu: 493 | fcall.extension = self.decS().decode('utf-8') 494 | elif fcall.type == Tread: 495 | fcall.fid = self.dec4() 496 | fcall.offset = self.dec8() 497 | fcall.count = self.dec4() 498 | elif fcall.type == Rread: 499 | fcall.data = self.decD() 500 | elif fcall.type == Twrite: 501 | fcall.fid = self.dec4() 502 | fcall.offset = self.dec8() 503 | fcall.count = self.dec4() 504 | fcall.data = self.buf.read(fcall.count) 505 | elif fcall.type == Rwrite: 506 | fcall.count = self.dec4() 507 | elif fcall.type in (Tclunk, Tremove, Tstat): 508 | fcall.fid = self.dec4() 509 | elif fcall.type in (Rstat, Twstat): 510 | if fcall.type == Twstat: 511 | fcall.fid = self.dec4() 512 | self.decstat(fcall.stat, 1) 513 | 514 | return fcall 515 | 516 | 517 | def modetostr(mode): 518 | bits = ["---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx"] 519 | 520 | def b(s): 521 | return bits[(mode >> s) & 7] 522 | d = "-" 523 | if mode & DMDIR: 524 | d = "d" 525 | elif mode & DMAPPEND: 526 | d = "a" 527 | return "%s%s%s%s" % (d, b(6), b(3), b(0)) 528 | 529 | 530 | def open2stat(mode): 531 | return (mode & 3) |\ 532 | ((mode & OAPPEND) >> 4) |\ 533 | ((mode & OEXCL) >> 5) |\ 534 | ((mode & OTRUNC) << 5) 535 | 536 | 537 | def open2plan(mode): 538 | return (mode & 3) |\ 539 | ((mode & os.O_APPEND) << 4) |\ 540 | ((mode & os.O_EXCL) << 5) |\ 541 | ((mode & os.O_TRUNC) >> 5) 542 | 543 | 544 | def mode2stat(mode): 545 | return (mode & 0o777) |\ 546 | ((mode & DMDIR ^ DMDIR) >> 16) |\ 547 | ((mode & DMDIR) >> 17) |\ 548 | ((mode & DMSYMLINK) >> 10) |\ 549 | ((mode & DMSYMLINK) >> 12) |\ 550 | ((mode & DMSETUID) >> 8) |\ 551 | ((mode & DMSETGID) >> 8) |\ 552 | ((mode & DMSTICKY) >> 7) 553 | 554 | 555 | def mode2plan(mode): 556 | return (mode & 0o777) | \ 557 | ((mode & stat.S_IFDIR) << 17) |\ 558 | ((mode & stat.S_ISUID) << 8) |\ 559 | ((mode & stat.S_ISGID) << 8) |\ 560 | ((mode & stat.S_ISVTX) << 7) |\ 561 | (int(mode == stat.S_IFLNK) << 25) 562 | 563 | 564 | def hash8(obj): 565 | return int(abs(hash(obj))) 566 | 567 | 568 | def otoa(p): 569 | '''Convert from open() to access()-style args''' 570 | ret = 0 571 | 572 | np = p & 3 573 | if np == OREAD: 574 | ret = AREAD 575 | elif np == OWRITE: 576 | ret = AWRITE 577 | elif np == ORDWR: 578 | ret = AREAD | AWRITE 579 | elif np == OEXEC: 580 | ret = AEXEC 581 | 582 | if(p & OTRUNC): 583 | ret |= AWRITE 584 | 585 | return ret 586 | 587 | 588 | def hasperm(f, uid, p): 589 | '''Verify permissions for access type 'p' to file 'f'. 'p' is of the type 590 | returned by otoa() above, i.e., should contain the A* flags. 591 | 592 | f should resemble Dir, i.e., should have f.mode, f.uid, f.gid''' 593 | m = f.mode & 7 # other 594 | if (p & m) == p: 595 | return 1 596 | 597 | if f.uid == uid: 598 | m |= (f.mode >> 6) & 7 599 | if (p & m) == p: 600 | return 1 601 | if f.gid == uid: 602 | m |= (f.mode >> 3) & 7 603 | if (p & m) == p: 604 | return 1 605 | return 0 606 | 607 | 608 | class Sock(object): 609 | """Per-connection state and appropriate read and write methods 610 | for the Marshaller.""" 611 | 612 | def __init__(self, sock, dotu=0, chatty=0): 613 | self.sock = sock 614 | self.fids = {} # fids are per client 615 | self.reqs = {} # reqs are per client 616 | self.uname = None 617 | self.closing = False 618 | self.marshal = Marshal9P(dotu=dotu, chatty=chatty) 619 | 620 | def send(self, x): 621 | self.marshal.send(self, x) 622 | 623 | def recv(self): 624 | return self.marshal.recv(self) 625 | 626 | def read(self, l): 627 | if self.closing: 628 | return "" 629 | x = self.sock.recv(l) 630 | while len(x) < l: 631 | b = self.sock.recv(l - len(x)) 632 | if not b: 633 | raise EofError("client eof") 634 | x += b 635 | return x 636 | 637 | def write(self, buf): 638 | if self.closing: 639 | return len(buf) 640 | if self.sock.send(buf) != len(buf): 641 | raise Error("short write") 642 | 643 | def fileno(self): 644 | return self.sock.fileno() 645 | 646 | def delfid(self, fid): 647 | if fid in self.fids: 648 | self.fids[fid].ref = self.fids[fid].ref - 1 649 | if self.fids[fid].ref == 0: 650 | del self.fids[fid] 651 | 652 | def getfid(self, fid): 653 | if fid in self.fids: 654 | return self.fids[fid] 655 | return None 656 | 657 | def close(self): 658 | self.sock.close() 659 | 660 | 661 | class Fcall(object): 662 | '''# possible values, from p9p's fcall.h 663 | msize # Tversion, Rversion 664 | version # Tversion, Rversion 665 | oldtag # Tflush 666 | ename # Rerror 667 | qid # Rattach, Ropen, Rcreate 668 | iounit # Ropen, Rcreate 669 | aqid # Rauth 670 | afid # Tauth, Tattach 671 | uname # Tauth, Tattach 672 | aname # Tauth, Tattach 673 | perm # Tcreate 674 | name # Tcreate 675 | mode # Tcreate, Topen 676 | newfid # Twalk 677 | nwname # Twalk 678 | wname # Twalk, array 679 | nwqid # Rwalk 680 | wqid # Rwalk, array 681 | offset # Tread, Twrite 682 | count # Tread, Twrite, Rread 683 | data # Twrite, Rread 684 | nstat # Twstat, Rstat 685 | stat # Twstat, Rstat 686 | 687 | # dotu extensions: 688 | errno # Rerror 689 | extension # Tcreate 690 | ''' 691 | 692 | def __init__(self, ftype, tag=1, fid=None): 693 | self.type = ftype 694 | self.fid = fid 695 | self.tag = tag 696 | self.stat = [] 697 | self.iounit = 8192 698 | self.ename = None 699 | self.wqid = None 700 | 701 | def tostr(self): 702 | attr = [x for x in dir(self) if not x.startswith('_') and 703 | not x.startswith('tostr')] 704 | 705 | ret = ' '.join("%s=%s" % (x, getattr(self, x)) for x in attr) 706 | ret = cmdName[self.type] + " " + ret 707 | 708 | return repr(ret) 709 | 710 | 711 | class Qid(object): 712 | 713 | def __init__(self, qtype=None, vers=None, path=None): 714 | self.type = qtype 715 | self.vers = vers 716 | self.path = path 717 | 718 | def __str__(self): 719 | return '(%x,%x,%x)' % (self.type, self.vers, self.path) 720 | 721 | __repr__ = __str__ 722 | 723 | 724 | class Fid(object): 725 | 726 | def __init__(self, pool, fid, path='', auth=0): 727 | if fid in pool: 728 | raise EdupfidError(Edupfid) 729 | self.fid = fid 730 | self.ref = 1 731 | self.omode = -1 732 | self.auth = auth 733 | self.uid = None 734 | self.qid = None 735 | self.path = path 736 | 737 | pool[fid] = self 738 | 739 | 740 | class Dir(object): 741 | # type: server type 742 | # dev server subtype 743 | # 744 | # file data: 745 | # qid unique id from server 746 | # mode permissions 747 | # atime last read time 748 | # mtime last write time 749 | # length file length 750 | # name 751 | # uid owner name 752 | # gid group name 753 | # muid last modifier name 754 | # 755 | # 9P2000.u extensions: 756 | # uidnum numeric uid 757 | # gidnum numeric gid 758 | # muidnum numeric muid 759 | # *ext extended info 760 | 761 | def __init__(self, dotu=0, *args, **kwargs): 762 | self.dotu = dotu 763 | self.statsz = 0 764 | # the dotu arguments will be added separately. this is not 765 | # straightforward but is cleaner. 766 | if len(args): 767 | (self.type, 768 | self.dev, 769 | self.qid, 770 | self.mode, 771 | self.atime, 772 | self.mtime, 773 | self.length, 774 | self.name, 775 | self.uid, 776 | self.gid, 777 | self.muid) = args[:11] 778 | 779 | if dotu: 780 | (self.extension, 781 | self.uidnum, 782 | self.gidnum, 783 | self.muidnum) = args[11:15] 784 | 785 | if len(kwargs.keys()): 786 | for i in kwargs.keys(): 787 | setattr(self, i, kwargs[i]) 788 | 789 | if not dotu: 790 | (self.extension, 791 | self.uidnum, 792 | self.gidnum, 793 | self.muidnum) = "", UIDUNDEF, UIDUNDEF, UIDUNDEF 794 | 795 | def tolstr(self, dirname=''): 796 | if dirname != '': 797 | dirname = dirname + '/' 798 | if self.dotu: 799 | return "%s %d %d %-8d\t\t%s%s" % ( 800 | modetostr(self.mode), self.uidnum, self.gidnum, 801 | self.length, dirname, self.name) 802 | else: 803 | return "%s %s %s %-8d\t\t%s%s" % ( 804 | modetostr(self.mode), self.uid, self.gid, 805 | self.length, dirname, self.name) 806 | 807 | def todata(self, marsh): 808 | marsh.setBuffer() 809 | marsh.encstat((self, ), 0) 810 | return marsh.buf.getvalue() 811 | 812 | 813 | class Req(object): 814 | def __init__(self, tag, fd=None, ifcall=None, ofcall=None, 815 | dir=None, oldreq=None, fid=None, afid=None, newfid=None): 816 | self.tag = tag 817 | self.fd = fd 818 | self.ifcall = ifcall 819 | self.ofcall = ofcall 820 | self.dir = dir 821 | self.oldreq = oldreq 822 | self.fid = fid 823 | self.afid = afid 824 | self.newfid = newfid 825 | 826 | 827 | class Server(object): 828 | """ 829 | A server interface to the protocol. 830 | Subclass this to provide service 831 | """ 832 | chatty = False 833 | readpool = [] 834 | writepool = [] 835 | activesocks = {} 836 | 837 | def __init__(self, listen, authmode=None, fs=None, user=None, 838 | dom=None, key=None, chatty=False, dotu=False, msize=8192): 839 | self.msize = msize 840 | 841 | if authmode is None: 842 | self.authfs = None 843 | elif authmode == 'pki': 844 | from py9p import pki 845 | self.authfs = pki.AuthFs(key) 846 | else: 847 | raise ServerError("unsupported auth mode") 848 | 849 | self.fs = fs 850 | self.authmode = authmode 851 | self.dotu = dotu 852 | 853 | self.readpool = [] 854 | self.writepool = [] 855 | self.deferread = {} 856 | self.deferwrite = {} 857 | self.user = user 858 | self.dom = dom 859 | self.host = listen[0] 860 | self.port = listen[1] 861 | self.chatty = chatty 862 | 863 | if self.host[0] == '/': 864 | self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 865 | try: 866 | os.unlink(self.host) 867 | except OSError: 868 | pass 869 | self.sock.bind(self.host) 870 | os.chmod(self.host, self.port) 871 | else: 872 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 873 | self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 874 | self.sock.bind((self.host, self.port),) 875 | self.sock.listen(5) 876 | self.readpool.append(self.sock) 877 | if self.chatty: 878 | print("listening to %s:%d" % (self.host, self.port)) 879 | 880 | def mount(self, fs): 881 | # XXX: for now only allow one mount 882 | # in the future accept fs/root and 883 | # handle different filesystems at walk time 884 | self.fs = fs 885 | 886 | def shutdown(self, sock): 887 | """Close down a connection.""" 888 | if sock not in self.activesocks: 889 | return 890 | s = self.activesocks[sock] 891 | assert not s.closing # we looped! 892 | s.closing = True 893 | 894 | if sock in self.readpool: 895 | self.readpool.remove(sock) 896 | if sock in self.writepool: 897 | self.writepool.remove(sock) 898 | 899 | # find first tag not in use 900 | tags = [r.ifcall.tag for r in s.reqs] 901 | tag = [n for n in range(1, 65535) if n not in tags][0] 902 | 903 | # flush all outstanding requests 904 | for r in s.reqs: 905 | req = Req(tag) 906 | req.ifcall = Fcall(Tflush, tag=tag, oldtag=r.ifcall.tag) 907 | req.ofcall = Fcall(Rflush, tag=tag) 908 | req.fd = s.fileno() 909 | req.sock = s 910 | self.tflush(req) 911 | 912 | # clunk all open fids 913 | fids = list(s.fids.keys()) 914 | for fid in fids: 915 | req = Req(tag) 916 | req.ifcall = Fcall(Tclunk, tag=tag, fid=fid) 917 | req.ofcall = Fcall(Rclunk, tag=tag) 918 | req.fd = s.fileno() 919 | req.sock = s 920 | self.tclunk(req) 921 | 922 | # flush should have taken care of this 923 | assert sock not in self.deferwrite and sock not in self.deferread 924 | 925 | sock.close() 926 | del self.activesocks[sock] 927 | 928 | def serve(self): 929 | while len(self.readpool) > 0 or len(self.writepool) > 0: 930 | inr, outr, excr = select.select(self.readpool, self.writepool, []) 931 | for s in outr: 932 | if s in self.deferwrite: 933 | # this is a fs-delayed req that's just become ready, 934 | req = self.deferwrite[s] 935 | self.unregwritefd(s) 936 | name = cmdName[req.ifcall.type][1:] 937 | try: 938 | func = getattr(self.fs, name) 939 | func(self, req) 940 | except: 941 | print("error in delayed write response: %s") 942 | traceback.print_exc() 943 | self.respond(req, "error in delayed response") 944 | continue 945 | for s in inr: 946 | if s == self.sock: 947 | cl, addr = s.accept() 948 | self.readpool.append(cl) 949 | self.activesocks[cl] = Sock(cl, self.dotu, self.chatty) 950 | if self.chatty: 951 | print("accepted connection from: %s" % str(addr)) 952 | else: 953 | if s in self.deferread: 954 | # this is a fs-delayed req that's just become ready, 955 | req = self.deferread[s] 956 | self.unregreadfd(s) 957 | name = cmdName[req.ifcall.type][1:] 958 | try: 959 | func = getattr(self.fs, name) 960 | func(self, req) 961 | except: 962 | print("error in delayed read response: %s") 963 | traceback.print_exc() 964 | self.respond(req, "error in delayed response") 965 | continue 966 | try: 967 | self.fromnet(self.activesocks[s]) 968 | except socket.error as e: 969 | if self.chatty: 970 | print("socket error: %s" % (e.args[1])) 971 | traceback.print_exc() 972 | self.shutdown(s) 973 | except EofError as e: 974 | if self.chatty: 975 | print("socket closed: %s" % (e.args[0])) 976 | self.readpool.remove(s) 977 | self.shutdown(s) 978 | except Exception as e: 979 | print("error in fromnet (protocol botch?)") 980 | traceback.print_exc() 981 | print("dropping connection...") 982 | self.shutdown(s) 983 | 984 | if self.chatty: 985 | print("main socket closed") 986 | 987 | return 988 | 989 | def respond(self, req, error=None, errno=None): 990 | name = 'r' + cmdName[req.ifcall.type][1:] 991 | if hasattr(self, name): 992 | func = getattr(self, name) 993 | try: 994 | func(req, error) 995 | except Exception as e: 996 | print("error in respond: ") 997 | traceback.print_exc() 998 | return -1 999 | else: 1000 | raise ServerError("can not handle message type " + 1001 | cmdName[req.ifcall.type]) 1002 | 1003 | req.ofcall.tag = req.ifcall.tag 1004 | if error: 1005 | req.ofcall.type = Rerror 1006 | req.ofcall.ename = c9.bytes3(error) 1007 | if not errno: 1008 | errno = ERRUNDEF 1009 | req.ofcall.errno = errno 1010 | s = req.sock 1011 | try: 1012 | s.send(req.ofcall) 1013 | except socket.error as e: 1014 | if self.chatty: 1015 | print("socket error: %s" % (e.args[1])) 1016 | traceback.print_exc() 1017 | self.shutdown(s) 1018 | except EofError as e: 1019 | if self.chatty: 1020 | print("socket closed: %s" % (e.args[0])) 1021 | self.shutdown(s) 1022 | except Exception as e: 1023 | if self.chatty: 1024 | print("socket error: %s" % (str(e.args))) 1025 | traceback.print_exc() 1026 | self.shutdown(s) 1027 | 1028 | # XXX: unsure whether we need proper flushing semantics from rsc's p9p 1029 | # thing is, we're not threaded. 1030 | 1031 | def fromnet(self, fd): 1032 | fcall = fd.recv() 1033 | req = Req(fcall.tag) 1034 | req.ifcall = fcall 1035 | req.ofcall = Fcall(fcall.type + 1, fcall.tag) 1036 | req.fd = fd.fileno() 1037 | req.sock = fd 1038 | 1039 | if req.ifcall.type not in cmdName: 1040 | self.respond(req, "invalid message") 1041 | 1042 | name = "t" + cmdName[req.ifcall.type][1:] 1043 | if hasattr(self, name): 1044 | func = getattr(self, name) 1045 | try: 1046 | func(req) 1047 | except Error as e: 1048 | if self.chatty: 1049 | traceback.print_exc() 1050 | self.respond(req, str(e.args[0][1]), e.args[0][0]) 1051 | except Exception as e: 1052 | if self.chatty: 1053 | traceback.print_exc() 1054 | self.respond(req, 'unhandled internal exception: ' + 1055 | str(e.args[0])) 1056 | else: 1057 | self.respond(req, "unhandled message: %s" % ( 1058 | cmdName[req.ifcall.type])) 1059 | return 1060 | 1061 | def regreadfd(self, fd, req): 1062 | '''Register a file descriptor in the read pool. When a fileserver 1063 | wants to delay responding to a message they can register an fd and 1064 | have it polled for reading. When it's ready, the corresponding 'req' 1065 | will be called''' 1066 | self.deferread[fd] = req 1067 | self.readpool.append(fd) 1068 | 1069 | def regwritefd(self, fd, req): 1070 | '''Register a file descriptor in the write pool.''' 1071 | self.deferwrite[fd] = req 1072 | self.writepool.append(fd) 1073 | 1074 | def unregreadfd(self, fd): 1075 | '''Delete a fd registered with regreadfd().''' 1076 | del self.deferread[fd] 1077 | self.readpool.remove(fd) 1078 | 1079 | def unregwritefd(self, fd): 1080 | '''Delete a fd registered with regwritefd().''' 1081 | del self.deferwrite[fd] 1082 | self.writepool.remove(fd) 1083 | 1084 | def tversion(self, req): 1085 | if req.ifcall.version[0:2] != b'9P': 1086 | req.ofcall.version = "unknown" 1087 | self.respond(req, None) 1088 | return 1089 | 1090 | if req.ifcall.version == versionu: 1091 | # dotu is passed to server init to indicate whether dotu 1092 | # will be supported 1093 | # 1094 | # if the server init code was told not to implement dotu 1095 | # then even if the remote wants dotu we must fall back to 9P2000 1096 | if self.dotu: 1097 | req.ofcall.version = versionu 1098 | else: 1099 | req.ofcall.version = version 1100 | else: 1101 | # if somebody requested a later version of the protocol 1102 | # (9Pxxxx.y, for xxxx>2000) then fall back to what we know 1103 | # best: 9P2000; 1104 | # 1105 | # if somebody requested 9Pxxxx for xxxx<2000 then we have no 1106 | # clue what to say and we just keep repeating the same. 1107 | req.ofcall.version = version 1108 | req.sock.marshal.dotu = 0 1109 | 1110 | req.ofcall.msize = min(req.ifcall.msize, self.msize) 1111 | self.respond(req, None) 1112 | 1113 | def rversion(self, req, error): 1114 | # self.msize = req.ofcall.msize 1115 | pass 1116 | 1117 | def tauth(self, req): 1118 | if self.authfs is None: 1119 | self.respond(req, "%s: authentication not required" % 1120 | (sys.argv[0])) 1121 | return 1122 | 1123 | try: 1124 | req.afid = Fid(req.sock.fids, req.ifcall.afid, auth=1) 1125 | except EdupfidError: 1126 | self.respond(req, Edupfid) 1127 | return 1128 | req.afid.uname = req.ifcall.uname 1129 | self.authfs.estab(req.afid) 1130 | req.afid.qid = Qid(QTAUTH, 0, hash8('#a')) 1131 | req.ofcall.aqid = req.afid.qid 1132 | self.respond(req, None) 1133 | 1134 | def rauth(self, req, error): 1135 | if error and req.afid: 1136 | req.sock.delfid(req.afid.fid) 1137 | 1138 | def tattach(self, req): 1139 | try: 1140 | req.fid = Fid(req.sock.fids, req.ifcall.fid) 1141 | except EdupfidError: 1142 | self.respond(req, Edupfid) 1143 | return 1144 | 1145 | req.afid = None 1146 | if req.ifcall.afid != NOFID: 1147 | req.afid = req.sock.fids[req.ifcall.afid] 1148 | if not req.afid: 1149 | self.respond(req, Eunknownfid) 1150 | return 1151 | if req.afid.suid != req.ifcall.uname: 1152 | self.respond(req, "not authenticated as %r" % req.ifcall.uname) 1153 | return 1154 | elif self.chatty: 1155 | print("authenticated as %r" % req.ifcall.uname) 1156 | elif self.authmode is not None: 1157 | self.respond(req, 'authentication not complete') 1158 | 1159 | req.fid.uid = req.ifcall.uname 1160 | req.sock.uname = req.ifcall.uname # now we know who we are 1161 | if hasattr(self.fs, 'attach'): 1162 | self.fs.attach() 1163 | else: 1164 | req.ofcall.qid = self.fs.root.qid 1165 | req.fid.qid = self.fs.root.qid 1166 | self.respond(req, None) 1167 | return 1168 | 1169 | def rattach(self, req, error): 1170 | if error and req.fid: 1171 | req.sock.delfid(req.fid.fid) 1172 | 1173 | def tflush(self, req): 1174 | if hasattr(self.fs, 'flush'): 1175 | self.fs.flush(self, req) 1176 | else: 1177 | req.sock.reqs = [] 1178 | self.respond(req, None) 1179 | 1180 | def rflush(self, req, error): 1181 | if req.oldreq: 1182 | if req.oldreq.responded == 0: 1183 | req.oldreq.nflush = req.oldreq.nflush + 1 1184 | if not hasattr(req.oldreq, 'flush'): 1185 | req.oldreq.nflush = 0 1186 | req.oldreq.flush = [] 1187 | req.oldreq.nflush = req.oldreq.nflush + 1 1188 | req.oldreq.flush.append(req) 1189 | req.oldreq = None 1190 | return 0 1191 | 1192 | def twalk(self, req): 1193 | req.ofcall.wqid = [] 1194 | 1195 | req.fid = req.sock.getfid(req.ifcall.fid) 1196 | if not req.fid: 1197 | self.respond(req, Eunknownfid) 1198 | return 1199 | if req.fid.omode != -1: 1200 | self.respond(req, "cannot clone open fid") 1201 | return 1202 | if len(req.ifcall.wname) and not (req.fid.qid.type & QTDIR): 1203 | self.respond(req, Ewalknotdir) 1204 | return 1205 | if req.ifcall.fid != req.ifcall.newfid: 1206 | try: 1207 | req.newfid = Fid(req.sock.fids, req.ifcall.newfid) 1208 | except EdupfidError: 1209 | self.respond(req, Edupfid) 1210 | return 1211 | req.newfid.uid = req.fid.uid 1212 | else: 1213 | req.fid.ref = req.fid.ref + 1 1214 | req.newfid = req.fid 1215 | 1216 | if len(req.ifcall.wname) == 0: 1217 | req.ofcall.nwqid = 0 1218 | self.respond(req, None) 1219 | elif hasattr(self.fs, 'walk'): 1220 | self.fs.walk(self, req) 1221 | else: 1222 | self.respond(req, "no walk function") 1223 | 1224 | def rwalk(self, req, error): 1225 | if error or (len(req.ofcall.wqid) < len(req.ifcall.wname) and 1226 | len(req.ifcall.wname) > 0): 1227 | if req.ifcall.fid != req.ifcall.newfid and req.newfid: 1228 | req.sock.delfid(req.ifcall.newfid) 1229 | if len(req.ofcall.wqid) == 0: 1230 | if not error and len(req.ifcall.wname) != 0: 1231 | req.error = Enotfound 1232 | else: 1233 | req.error = None 1234 | else: 1235 | if len(req.ofcall.wqid) == 0: 1236 | req.newfid.qid = req.fid.qid 1237 | else: 1238 | req.newfid.qid = req.ofcall.wqid[-1] 1239 | 1240 | def topen(self, req): 1241 | req.fid = req.sock.getfid(req.ifcall.fid) 1242 | if not req.fid: 1243 | self.respond(req, Eunknownfid) 1244 | return 1245 | if req.fid.omode != -1: 1246 | self.respond(req, Ebotch) 1247 | return 1248 | if req.fid.qid.type & QTDIR: 1249 | if (req.ifcall.mode & (~ORCLOSE)) != OREAD: 1250 | self.respond(req, Eisdir) 1251 | return 1252 | # repeating the same bug as p9p? 1253 | if otoa(req.ifcall.mode) != AREAD: 1254 | self.respond(req, Eisdir) 1255 | return 1256 | 1257 | req.ofcall.qid = req.fid.qid 1258 | req.ofcall.iounit = self.msize - IOHDRSZ 1259 | req.ifcall.acc = [AREAD, AWRITE, 1260 | AREAD | AWRITE, AEXEC][req.ifcall.mode & 3] 1261 | if req.ifcall.mode & OTRUNC: 1262 | req.ifcall.acc |= AWRITE 1263 | 1264 | if (req.fid.qid.type & QTDIR) and (req.ifcall.acc != AREAD): 1265 | self.respond(req, Eperm) 1266 | if hasattr(self.fs, 'open'): 1267 | self.fs.open(self, req) 1268 | else: 1269 | self.respond(req, None) 1270 | 1271 | def ropen(self, req, error): 1272 | if error: 1273 | return 1274 | req.fid.omode = req.ifcall.mode 1275 | req.fid.qid = req.ofcall.qid 1276 | if req.ofcall.qid.type & QTDIR: 1277 | req.fid.diroffset = 0 1278 | 1279 | def tcreate(self, req): 1280 | req.fid = req.sock.getfid(req.ifcall.fid) 1281 | if not req.fid: 1282 | self.respond(req, Eunknownfid) 1283 | elif req.fid.omode != -1: 1284 | self.respond(req, Ebotch) 1285 | elif not (req.fid.qid.type & QTDIR): 1286 | self.respond(req, Ecreatenondir) 1287 | elif hasattr(self.fs, 'create'): 1288 | self.fs.create(self, req) 1289 | else: 1290 | self.respond(req, Enocreate) 1291 | 1292 | def rcreate(self, req, error): 1293 | if error: 1294 | return 1295 | req.fid.omode = req.ifcall.mode 1296 | req.fid.qid = req.ofcall.qid 1297 | req.ofcall.iounit = self.msize - IOHDRSZ 1298 | 1299 | def bufread(self, req, buf): 1300 | req.ofcall.data = buf[req.ifcall.offset: req.ifcall.offset + 1301 | req.ifcall.count] 1302 | return self.respond(req, None) 1303 | 1304 | def tread(self, req): 1305 | req.fid = req.sock.getfid(req.ifcall.fid) 1306 | if not req.fid: 1307 | return self.respond(req, Eunknownfid) 1308 | if req.ifcall.count < 0: 1309 | return self.respond(req, Ebotch) 1310 | if req.ifcall.offset < 0 or ((req.fid.qid.type & QTDIR) and 1311 | (req.ifcall.offset != 0) and 1312 | (req.ifcall.offset != req.fid.diroffset)): 1313 | return self.respond(req, Ebadoffset) 1314 | if req.fid.qid.type & QTAUTH and self.authfs: 1315 | self.authfs.read(self, req) 1316 | return 1317 | # auth Tread goes w/o omode, there was no open() 1318 | if req.fid.omode == -1: 1319 | return self.respond(req, Eopen) 1320 | 1321 | if req.ifcall.count > self.msize - IOHDRSZ: 1322 | req.ifcall.count = self.msize - IOHDRSZ 1323 | o = req.fid.omode & 3 1324 | if o != OREAD and o != ORDWR and o != OEXEC: 1325 | return self.respond(req, Ebotch) 1326 | if hasattr(self.fs, 'read'): 1327 | self.fs.read(self, req) 1328 | else: 1329 | self.respond(req, 'no server read function') 1330 | 1331 | def rread(self, req, error): 1332 | if error: 1333 | return 1334 | 1335 | if req.fid.qid.type & QTDIR: 1336 | data = b"" 1337 | for x in req.ofcall.stat: 1338 | ndata = x.todata(req.sock.marshal) 1339 | if (len(data) - req.ifcall.offset) + \ 1340 | len(ndata) < req.ifcall.count: 1341 | data = data + ndata 1342 | else: 1343 | break 1344 | req.ofcall.data = data[req.ifcall.offset:] 1345 | req.fid.diroffset = req.ifcall.offset + len(req.ofcall.data) 1346 | 1347 | def twrite(self, req): 1348 | req.fid = req.sock.getfid(req.ifcall.fid) 1349 | if not req.fid: 1350 | return self.respond(req, Eunknownfid) 1351 | if req.ifcall.count < 0 or req.ifcall.offset < 0: 1352 | return self.respond(req, Ebotch) 1353 | if req.fid.qid.type & QTAUTH and self.authfs: 1354 | self.authfs.write(self, req) 1355 | return 1356 | # auth Tread goes w/o omode, there was no open() 1357 | if req.fid.omode == -1: 1358 | return self.respond(req, Eopen) 1359 | 1360 | if req.ifcall.count > self.msize - IOHDRSZ: 1361 | req.ifcall.count = self.msize - IOHDRSZ 1362 | o = req.fid.omode & 3 1363 | if o != OWRITE and o != ORDWR: 1364 | return self.respond(req, 1365 | "write on fid with open mode 0x%ux" % req.fid.omode) 1366 | if hasattr(self.fs, 'write'): 1367 | self.fs.write(self, req) 1368 | else: 1369 | self.respond(req, 'no server write function') 1370 | 1371 | def rwrite(self, req, error): 1372 | return 1373 | 1374 | def tclunk(self, req): 1375 | req.fid = req.sock.getfid(req.ifcall.fid) 1376 | if not req.fid: 1377 | return self.respond(req, Eunknownfid) 1378 | if hasattr(self.fs, 'clunk') and not (req.fid.qid.type & QTAUTH): 1379 | self.fs.clunk(self, req) 1380 | else: 1381 | self.respond(req, None) 1382 | req.sock.delfid(req.ifcall.fid) 1383 | 1384 | def rclunk(self, req, error): 1385 | return 1386 | 1387 | def tremove(self, req): 1388 | req.fid = req.sock.getfid(req.ifcall.fid) 1389 | if not req.fid: 1390 | return self.respond(req, Eunknownfid) 1391 | if hasattr(self.fs, 'remove'): 1392 | self.fs.remove(self, req) 1393 | else: 1394 | self.respond(req, Enoremove) 1395 | 1396 | def rremove(self, req, error): 1397 | req.sock.delfid(req.ifcall.fid) 1398 | return 1399 | 1400 | def tstat(self, req): 1401 | req.fid = req.sock.getfid(req.ifcall.fid) 1402 | req.ofcall.stat = [] 1403 | if not req.fid: 1404 | return self.respond(req, Eunknownfid) 1405 | if hasattr(self.fs, 'stat'): 1406 | self.fs.stat(self, req) 1407 | else: 1408 | self.respond(req, Enostat) 1409 | 1410 | def rstat(self, req, error): 1411 | if error: 1412 | return 1413 | 1414 | def twstat(self, req): 1415 | req.fid = req.sock.getfid(req.ifcall.fid) 1416 | if not req.fid: 1417 | return self.respond(req, Eunknownfid) 1418 | if hasattr(self.fs, 'wstat'): 1419 | self.fs.wstat(self, req) 1420 | else: 1421 | self.respond(req, Enowstat) 1422 | 1423 | def rwstat(self, req, error): 1424 | return 1425 | 1426 | 1427 | class Credentials(object): 1428 | def __init__(self, user, authmode=None, passwd=None, 1429 | keyfile=None, key=None): 1430 | self.user = c9.bytes3(user) if isinstance(user, str) else user 1431 | self.passwd = c9.bytes3(passwd) if isinstance(passwd, str) else passwd 1432 | self.key = key 1433 | self.authmode = authmode 1434 | if self.authmode == "pki": 1435 | import pki 1436 | self.key = pki.getprivkey(user, keyfile, passwd) 1437 | 1438 | 1439 | class Client(object): 1440 | """ 1441 | A client interface to the protocol. 1442 | """ 1443 | AFID = 10 1444 | ROOT = 11 1445 | CWD = 12 1446 | F = 13 1447 | 1448 | path = '' # for 'getwd' equivalent 1449 | 1450 | def __init__(self, fd, credentials, authsrv=None, chatty=0, dotu=0, 1451 | msize=8192): 1452 | self.credentials = credentials 1453 | self.dotu = dotu 1454 | self.msize = msize 1455 | self.fd = Sock(fd, dotu, chatty) 1456 | self.login(authsrv, credentials) 1457 | 1458 | def _rpc(self, fcall): 1459 | if fcall.type == Tversion: 1460 | fcall.tag = NOTAG 1461 | self.fd.send(fcall) 1462 | try: 1463 | ifcall = self.fd.recv() 1464 | except (KeyboardInterrupt, Exception): 1465 | # try to flush the operation, then rethrow exception 1466 | if fcall.type != Tflush: 1467 | try: 1468 | self._flush(fcall.tag, fcall.tag + 1) 1469 | except Exception: 1470 | pass 1471 | raise 1472 | if ifcall.tag != fcall.tag: 1473 | raise RpcError("invalid tag received") 1474 | if ifcall.type == Rerror: 1475 | raise RpcError(ifcall.ename) 1476 | if ifcall.type != fcall.type + 1: 1477 | raise ClientError("incorrect reply from server: %r" % 1478 | [fcall.type, fcall.tag]) 1479 | return ifcall 1480 | 1481 | # protocol calls; part of 9p 1482 | # should be private functions, really 1483 | def _version(self, msize, version): 1484 | fcall = Fcall(Tversion) 1485 | self.msize = msize 1486 | fcall.msize = msize 1487 | fcall.version = version 1488 | return self._rpc(fcall) 1489 | 1490 | def _auth(self, afid, uname, aname): 1491 | fcall = Fcall(Tauth) 1492 | fcall.afid = afid 1493 | fcall.uname = uname 1494 | fcall.aname = aname 1495 | fcall.uidnum = 0 1496 | return self._rpc(fcall) 1497 | 1498 | def _attach(self, fid, afid, uname, aname): 1499 | fcall = Fcall(Tattach) 1500 | fcall.fid = fid 1501 | fcall.afid = afid 1502 | fcall.uname = uname 1503 | fcall.aname = aname 1504 | fcall.uidnum = 0 1505 | return self._rpc(fcall) 1506 | 1507 | def _walk(self, fid, newfid, wnames): 1508 | fcall = Fcall(Twalk) 1509 | fcall.fid = fid 1510 | fcall.newfid = newfid 1511 | fcall.wname = [c9.bytes3(x) for x in wnames] 1512 | return self._rpc(fcall) 1513 | 1514 | def _open(self, fid, mode): 1515 | fcall = Fcall(Topen) 1516 | fcall.fid = fid 1517 | fcall.mode = mode 1518 | return self._rpc(fcall) 1519 | 1520 | def _create(self, fid, name, perm, mode, extension=b""): 1521 | fcall = Fcall(Tcreate) 1522 | fcall.fid = fid 1523 | fcall.name = name 1524 | fcall.perm = perm 1525 | fcall.mode = mode 1526 | fcall.extension = extension 1527 | return self._rpc(fcall) 1528 | 1529 | def _read(self, fid, off, count): 1530 | fcall = Fcall(Tread) 1531 | fcall.fid = fid 1532 | fcall.offset = off 1533 | if count > self.msize - IOHDRSZ: 1534 | count = self.msize - IOHDRSZ 1535 | fcall.count = count 1536 | return self._rpc(fcall) 1537 | 1538 | def _write(self, fid, off, data): 1539 | fcall = Fcall(Twrite) 1540 | fcall.fid = fid 1541 | fcall.offset = off 1542 | fcall.data = data 1543 | return self._rpc(fcall) 1544 | 1545 | def _clunk(self, fid): 1546 | fcall = Fcall(Tclunk) 1547 | fcall.fid = fid 1548 | return self._rpc(fcall) 1549 | 1550 | def _remove(self, fid): 1551 | fcall = Fcall(Tremove) 1552 | fcall.fid = fid 1553 | return self._rpc(fcall) 1554 | 1555 | def _stat(self, fid): 1556 | fcall = Fcall(Tstat) 1557 | fcall.fid = fid 1558 | return self._rpc(fcall) 1559 | 1560 | def _wstat(self, fid, stats): 1561 | fcall = Fcall(Twstat) 1562 | fcall.fid = fid 1563 | fcall.stat = stats 1564 | return self._rpc(fcall) 1565 | 1566 | def _flush(self, tag, oldtag): 1567 | fcall = Fcall(Tflush, tag=tag) 1568 | fcall.oldtag = tag 1569 | return self._rpc(fcall) 1570 | 1571 | def _fullclose(self): 1572 | self._clunk(self.ROOT) 1573 | self._clunk(self.CWD) 1574 | self.fd.close() 1575 | 1576 | def login(self, authsrv, credentials): 1577 | if self.dotu: 1578 | ver = versionu 1579 | else: 1580 | ver = version 1581 | fcall = self._version(self.msize, ver) 1582 | self.msize = fcall.msize 1583 | if fcall.version != ver: 1584 | raise VersionError("version mismatch: %r" % fcall.version) 1585 | 1586 | fcall.afid = self.AFID 1587 | try: 1588 | rfcall = self._auth(fcall.afid, credentials.user, b'') 1589 | except RpcError as e: 1590 | fcall.afid = NOFID 1591 | 1592 | if fcall.afid != NOFID: 1593 | fcall.aqid = rfcall.aqid 1594 | 1595 | if credentials.authmode is None: 1596 | raise ClientError('no authentication method') 1597 | elif credentials.authmode == 'pki': 1598 | import pki 1599 | pki.clientAuth(self, fcall, credentials) 1600 | else: 1601 | raise ClientError('unknown authentication method: %s' % 1602 | credentials.authmode) 1603 | 1604 | self._attach(self.ROOT, fcall.afid, credentials.user, b'') 1605 | if fcall.afid != NOFID: 1606 | self._clunk(fcall.afid) 1607 | self._walk(self.ROOT, self.CWD, []) 1608 | self.path = '/' 1609 | 1610 | # user accessible calls, the actual implementation of a client 1611 | def close(self): 1612 | self._clunk(self.F) 1613 | 1614 | def walk(self, pstr=''): 1615 | root = self.CWD 1616 | if pstr == '': 1617 | path = [] 1618 | elif pstr.find('/') == -1: 1619 | path = [pstr] 1620 | else: 1621 | path = pstr.split('/') 1622 | if path[0] == '': 1623 | root = self.ROOT 1624 | path = path[1:] 1625 | path = list(filter(None, path)) 1626 | try: 1627 | fcall = self._walk(root, self.F, path) 1628 | except RpcError: 1629 | raise 1630 | 1631 | if len(fcall.wqid) < len(path): 1632 | raise RpcError('incomplete walk (%d out of %d)' % 1633 | (len(fcall.wqid), len(path))) 1634 | return fcall.wqid 1635 | 1636 | def open(self, pstr='', mode=0): 1637 | if self.walk(pstr) is None: 1638 | return 1639 | self.pos = 0 1640 | try: 1641 | fcall = self._open(self.F, mode) 1642 | except RpcError: 1643 | self.close() 1644 | raise 1645 | return fcall 1646 | 1647 | def create(self, pstr, perm=0o644, mode=1): 1648 | p = pstr.split('/') 1649 | pstr2, name = '/'.join(p[:-1]), p[-1] 1650 | if self.walk(pstr2) is None: 1651 | return 1652 | self.pos = 0 1653 | try: 1654 | return self._create(self.F, name, perm, mode) 1655 | except RpcError: 1656 | self.close() 1657 | raise 1658 | 1659 | def rm(self, pstr): 1660 | self.open(pstr) 1661 | try: 1662 | self._remove(self.F) 1663 | except RpcError: 1664 | raise 1665 | 1666 | def read(self, l): 1667 | try: 1668 | fcall = self._read(self.F, self.pos, l) 1669 | buf = fcall.data 1670 | except RpcError: 1671 | self.close() 1672 | raise 1673 | 1674 | self.pos += len(buf) 1675 | return buf 1676 | 1677 | def write(self, buf): 1678 | try: 1679 | l = self._write(self.F, self.pos, buf).count 1680 | self.pos += l 1681 | return l 1682 | except RpcError: 1683 | self.close() 1684 | raise 1685 | 1686 | def stat(self, pstr): 1687 | if self.walk(pstr) is None: 1688 | return 1689 | try: 1690 | fc = self._stat(self.F) 1691 | finally: 1692 | self.close() 1693 | return fc.stat 1694 | 1695 | def lsdir(self): 1696 | ret = [] 1697 | while 1: 1698 | buf = self.read(self.msize) 1699 | if len(buf) == 0: 1700 | break 1701 | p9 = Marshal9P() 1702 | p9.setBuffer(buf) 1703 | p9.buf.seek(0) 1704 | fcall = Fcall(Rstat) 1705 | try: 1706 | p9.decstat(fcall.stat, 0) 1707 | except: 1708 | self.close() 1709 | print('unexpected decstat error:') 1710 | traceback.print_exc() 1711 | raise 1712 | ret += fcall.stat 1713 | return ret 1714 | 1715 | def ls(self, long=0, args=[]): 1716 | ret = [] 1717 | 1718 | if len(args) == 0: 1719 | if self.open() is None: 1720 | return 1721 | if long: 1722 | ret = [z.tolstr() for z in self.lsdir()] 1723 | else: 1724 | ret = [z.name for z in self.lsdir()] 1725 | self.close() 1726 | else: 1727 | for x in args: 1728 | stat = self.stat(x) 1729 | if not stat: 1730 | return # stat already printed a message 1731 | if len(stat) == 1: 1732 | if stat[0].mode & DMDIR: 1733 | self.open(x) 1734 | lsd = self.lsdir() 1735 | if long: 1736 | ret += [z.tolstr() for z in lsd] 1737 | else: 1738 | ret += [x + '/' + z.name for z in lsd] 1739 | self.close() 1740 | else: 1741 | if long: 1742 | # we already have full path+name, but tolstr() 1743 | # wants to append the name to the end anyway, so 1744 | # strip the last basename out to form identical 1745 | # path+name 1746 | ret.append(stat[0].tolstr( 1747 | x[0:-len(stat[0].name) - 1])) 1748 | else: 1749 | ret.append(x) 1750 | else: 1751 | print('%s: returned multiple stats (internal error)' % x) 1752 | return ret 1753 | 1754 | def cd(self, pstr): 1755 | q = self.walk(pstr) 1756 | if q is None: 1757 | return 0 1758 | if q and not (q[-1].type & QTDIR): 1759 | print("%s: not a directory" % pstr) 1760 | self.close() 1761 | return 0 1762 | self.F, self.CWD = self.CWD, self.F 1763 | self.close() 1764 | return 1 1765 | --------------------------------------------------------------------------------