├── .gitignore ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── config ├── config.ini ├── config1.ini ├── config2.ini └── logging_config.ini ├── documentation ├── Makefile ├── make.bat └── source │ ├── conf.py │ └── index.rst ├── examples ├── __init__.py ├── connections_simulation.py ├── peer_request_response.py ├── receive_test.py └── send_test.py ├── gossip ├── __init__.py ├── communication │ ├── __init__.py │ ├── client_receiver.py │ ├── client_sender.py │ ├── connection.py │ └── server.py ├── control │ ├── __init__.py │ ├── api_controller.py │ ├── api_registrations.py │ ├── convert.py │ ├── message_cache.py │ └── p2p_controller.py ├── gossip_main.py └── util │ ├── __init__.py │ ├── byte_formatting.py │ ├── config_parser.py │ ├── exceptions.py │ ├── message.py │ ├── message_code.py │ ├── packing.py │ └── queue_item_types.py ├── main.py ├── requirements.txt ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── communication ├── __init__.py └── test_connection_pool.py ├── control ├── test_api_registrations.py └── test_message_cache.py ├── first_test.py └── util ├── __init__.py ├── test_byte_formatting.py └── test_message.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/latex,tex,pycharm,virtualenv 3 | 4 | ### LaTeX ### 5 | *.acn 6 | *.acr 7 | *.alg 8 | *.aux 9 | *.bbl 10 | *.bcf 11 | *.blg 12 | *.dvi 13 | *.fdb_latexmk 14 | *.fls 15 | *.glg 16 | *.glo 17 | *.gls 18 | *.idx 19 | *.ilg 20 | *.ind 21 | *.ist 22 | *.lof 23 | *.log 24 | *.lot 25 | *.maf 26 | *.mtc 27 | *.mtc0 28 | *.nav 29 | *.nlo 30 | *.out 31 | *.pdfsync 32 | *.ps 33 | *.run.xml 34 | *.snm 35 | *.synctex.gz 36 | *.toc 37 | *.vrb 38 | *.xdy 39 | *.tdo 40 | 41 | 42 | ### TeX ### 43 | ## Core latex/pdflatex auxiliary files: 44 | *.aux 45 | *.lof 46 | *.log 47 | *.lot 48 | *.fls 49 | *.out 50 | *.toc 51 | *.fmt 52 | *.fot 53 | *.cb 54 | *.cb2 55 | 56 | ## Intermediate documents: 57 | *.dvi 58 | *-converted-to.* 59 | # these rules might exclude image files for figures etc. 60 | # *.ps 61 | # *.eps 62 | # *.pdf 63 | 64 | ## Bibliography auxiliary files (bibtex/biblatex/biber): 65 | *.bbl 66 | *.bcf 67 | *.blg 68 | *-blx.aux 69 | *-blx.bib 70 | *.brf 71 | *.run.xml 72 | 73 | ## Build tool auxiliary files: 74 | *.fdb_latexmk 75 | *.synctex 76 | *.synctex.gz 77 | *.synctex.gz(busy) 78 | *.pdfsync 79 | 80 | ## Auxiliary and intermediate files from other packages: 81 | # algorithms 82 | *.alg 83 | *.loa 84 | 85 | # achemso 86 | acs-*.bib 87 | 88 | # amsthm 89 | *.thm 90 | 91 | # beamer 92 | *.nav 93 | *.snm 94 | *.vrb 95 | 96 | # cprotect 97 | *.cpt 98 | 99 | # fixme 100 | *.lox 101 | 102 | #(r)(e)ledmac/(r)(e)ledpar 103 | *.end 104 | *.?end 105 | *.[1-9] 106 | *.[1-9][0-9] 107 | *.[1-9][0-9][0-9] 108 | *.[1-9]R 109 | *.[1-9][0-9]R 110 | *.[1-9][0-9][0-9]R 111 | *.eledsec[1-9] 112 | *.eledsec[1-9]R 113 | *.eledsec[1-9][0-9] 114 | *.eledsec[1-9][0-9]R 115 | *.eledsec[1-9][0-9][0-9] 116 | *.eledsec[1-9][0-9][0-9]R 117 | 118 | # glossaries 119 | *.acn 120 | *.acr 121 | *.glg 122 | *.glo 123 | *.gls 124 | *.glsdefs 125 | 126 | # gnuplottex 127 | *-gnuplottex-* 128 | 129 | # hyperref 130 | *.brf 131 | 132 | # knitr 133 | *-concordance.tex 134 | # TODO Comment the next line if you want to keep your tikz graphics files 135 | *.tikz 136 | *-tikzDictionary 137 | 138 | # listings 139 | *.lol 140 | 141 | # makeidx 142 | *.idx 143 | *.ilg 144 | *.ind 145 | *.ist 146 | 147 | # minitoc 148 | *.maf 149 | *.mlf 150 | *.mlt 151 | *.mtc 152 | *.mtc[0-9] 153 | *.mtc[1-9][0-9] 154 | 155 | # minted 156 | _minted* 157 | *.pyg 158 | 159 | # morewrites 160 | *.mw 161 | 162 | # mylatexformat 163 | *.fmt 164 | 165 | # nomencl 166 | *.nlo 167 | 168 | # sagetex 169 | *.sagetex.sage 170 | *.sagetex.py 171 | *.sagetex.scmd 172 | 173 | # sympy 174 | *.sout 175 | *.sympy 176 | sympy-plots-for-*.tex/ 177 | 178 | # pdfcomment 179 | *.upa 180 | *.upb 181 | 182 | # pythontex 183 | *.pytxcode 184 | pythontex-files-*/ 185 | 186 | # thmtools 187 | *.loe 188 | 189 | # TikZ & PGF 190 | *.dpth 191 | *.md5 192 | *.auxlock 193 | 194 | # todonotes 195 | *.tdo 196 | 197 | # xindy 198 | *.xdy 199 | 200 | # xypic precompiled matrices 201 | *.xyc 202 | 203 | # endfloat 204 | *.ttt 205 | *.fff 206 | 207 | # Latexian 208 | TSWLatexianTemp* 209 | 210 | ## Editors: 211 | # WinEdt 212 | *.bak 213 | *.sav 214 | 215 | # Texpad 216 | .texpadtmp 217 | 218 | # Kile 219 | *.backup 220 | 221 | # KBibTeX 222 | *~[0-9]* 223 | 224 | ### TeX Patch ### 225 | # pdfcomment 226 | .upa 227 | .upb 228 | 229 | 230 | ### PyCharm ### 231 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 232 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 233 | 234 | # User-specific stuff: 235 | .idea/workspace.xml 236 | .idea/tasks.xml 237 | .idea/dictionaries 238 | .idea/vcs.xml 239 | .idea/jsLibraryMappings.xml 240 | 241 | # Sensitive or high-churn files: 242 | .idea/dataSources.ids 243 | .idea/dataSources.xml 244 | .idea/dataSources.local.xml 245 | .idea/sqlDataSources.xml 246 | .idea/dynamic.xml 247 | .idea/uiDesigner.xml 248 | 249 | # Gradle: 250 | .idea/gradle.xml 251 | .idea/libraries 252 | 253 | # Mongo Explorer plugin: 254 | .idea/mongoSettings.xml 255 | 256 | ## File-based project format: 257 | *.iws 258 | 259 | ## Plugin-specific files: 260 | 261 | # IntelliJ 262 | /out/ 263 | 264 | # mpeltonen/sbt-idea plugin 265 | .idea_modules/ 266 | 267 | # JIRA plugin 268 | atlassian-ide-plugin.xml 269 | 270 | # Crashlytics plugin (for Android Studio and IntelliJ) 271 | com_crashlytics_export_strings.xml 272 | crashlytics.properties 273 | crashlytics-build.properties 274 | fabric.properties 275 | 276 | ### PyCharm Patch ### 277 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 278 | 279 | # *.iml 280 | # modules.xml 281 | 282 | 283 | ### VirtualEnv ### 284 | # Virtualenv 285 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 286 | .Python 287 | [Bb]in 288 | [Ii]nclude 289 | [Ll]ib 290 | [Ll]ib64 291 | [Ll]ocal 292 | [Ss]cripts 293 | pyvenv.cfg 294 | .venv 295 | pip-selfcheck.json 296 | 297 | .idea/ 298 | documentation/_build/ 299 | documentation/build/ 300 | *.pyc 301 | .* 302 | 303 | # setup 304 | dist/ 305 | gossip_python.egg-info/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt 2 | include LICENSE 3 | recursive-include examples *.txt *.py 4 | prune examples/ -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | init: 2 | pip install -r requirements.txt 3 | 4 | test: 5 | py.test tests --html=documentation/_build/html/test_output.html 6 | 7 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | gossip-python 2 | ============= 3 | Documentation is available here: https://pythonhosted.org/gossip-python/ 4 | 5 | gossip-python is an implementation of the `gossip protocol `_ 6 | . It started as an university project for the lecture `Peer-to-Peer Systems and Security `_ at the Technical University of Munich (TUM). 7 | 8 | The authors of gossip-python are Thomas Maier (ga85how@mytum.de), Anselm Binninger (ga85dib@mytum.de) and Ralph O. Schaumann (ga65gis@mytum.de). 9 | 10 | Installation with pip: 11 | 12 | ``pip install gossip-python`` 13 | 14 | Please use the `documentation `_ for basic usage and examples. 15 | -------------------------------------------------------------------------------- /config/config.ini: -------------------------------------------------------------------------------- 1 | [GLOBAL] 2 | HOSTKEY = /hostkey.pem 3 | 4 | [GOSSIP] 5 | cache_size = 50 6 | max_connections = 30 7 | bootstrapper = 8 | listen_address = 192.168.1.20:6001 9 | api_address = 192.168.1.20:7001 10 | max_ttl = 0 11 | -------------------------------------------------------------------------------- /config/config1.ini: -------------------------------------------------------------------------------- 1 | [GLOBAL] 2 | HOSTKEY = /hostkey.pem 3 | 4 | [GOSSIP] 5 | cache_size = 50 6 | max_connections = 30 7 | bootstrapper = 192.168.1.20:6001 8 | listen_address = 192.168.1.20:6002 9 | api_address = 192.168.1.20:7002 10 | max_ttl = 0 11 | -------------------------------------------------------------------------------- /config/config2.ini: -------------------------------------------------------------------------------- 1 | [GLOBAL] 2 | HOSTKEY = /hostkey.pem 3 | 4 | [GOSSIP] 5 | cache_size = 50 6 | max_connections = 30 7 | bootstrapper = 192.168.1.20:6001 8 | listen_address = 192.168.1.20:6003 9 | api_address = 192.168.1.20:7003 10 | max_ttl = 0 11 | -------------------------------------------------------------------------------- /config/logging_config.ini: -------------------------------------------------------------------------------- 1 | [loggers] 2 | keys=root 3 | 4 | [handlers] 5 | keys=consoleHandler 6 | 7 | [formatters] 8 | keys=simpleFormatter 9 | 10 | [logger_root] 11 | level=DEBUG 12 | handlers=consoleHandler 13 | 14 | [handler_consoleHandler] 15 | class=StreamHandler 16 | level=DEBUG 17 | formatter=simpleFormatter 18 | args=(sys.stdout,) 19 | 20 | [formatter_simpleFormatter] 21 | format=%(asctime)s - %(name)s - %(levelname)s - %(message)s 22 | datefmt=%d.%m.%Y %H:%M:%S 23 | -------------------------------------------------------------------------------- /documentation/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don\'t have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " epub3 to make an epub3" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | @echo " dummy to check syntax errors of document sources" 51 | 52 | .PHONY: clean 53 | clean: 54 | rm -rf $(BUILDDIR)/* 55 | 56 | .PHONY: html 57 | html: 58 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 61 | 62 | .PHONY: dirhtml 63 | dirhtml: 64 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 65 | @echo 66 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 67 | 68 | .PHONY: singlehtml 69 | singlehtml: 70 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 71 | @echo 72 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 73 | 74 | .PHONY: pickle 75 | pickle: 76 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 77 | @echo 78 | @echo "Build finished; now you can process the pickle files." 79 | 80 | .PHONY: json 81 | json: 82 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 83 | @echo 84 | @echo "Build finished; now you can process the JSON files." 85 | 86 | .PHONY: htmlhelp 87 | htmlhelp: 88 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 89 | @echo 90 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 91 | ".hhp project file in $(BUILDDIR)/htmlhelp." 92 | 93 | .PHONY: qthelp 94 | qthelp: 95 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 96 | @echo 97 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 98 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 99 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/gossip-python.qhcp" 100 | @echo "To view the help file:" 101 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/gossip-python.qhc" 102 | 103 | .PHONY: applehelp 104 | applehelp: 105 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 106 | @echo 107 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 108 | @echo "N.B. You won't be able to view it unless you put it in" \ 109 | "~/Library/Documentation/Help or install it in your application" \ 110 | "bundle." 111 | 112 | .PHONY: devhelp 113 | devhelp: 114 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 115 | @echo 116 | @echo "Build finished." 117 | @echo "To view the help file:" 118 | @echo "# mkdir -p $$HOME/.local/share/devhelp/gossip-python" 119 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/gossip-python" 120 | @echo "# devhelp" 121 | 122 | .PHONY: epub 123 | epub: 124 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 125 | @echo 126 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 127 | 128 | .PHONY: epub3 129 | epub3: 130 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 131 | @echo 132 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 133 | 134 | .PHONY: latex 135 | latex: 136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 137 | @echo 138 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 139 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 140 | "(use \`make latexpdf' here to do that automatically)." 141 | 142 | .PHONY: latexpdf 143 | latexpdf: 144 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 145 | @echo "Running LaTeX files through pdflatex..." 146 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 147 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 148 | 149 | .PHONY: latexpdfja 150 | latexpdfja: 151 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 152 | @echo "Running LaTeX files through platex and dvipdfmx..." 153 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 154 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 155 | 156 | .PHONY: text 157 | text: 158 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 159 | @echo 160 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 161 | 162 | .PHONY: man 163 | man: 164 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 165 | @echo 166 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 167 | 168 | .PHONY: texinfo 169 | texinfo: 170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 171 | @echo 172 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 173 | @echo "Run \`make' in that directory to run these through makeinfo" \ 174 | "(use \`make info' here to do that automatically)." 175 | 176 | .PHONY: info 177 | info: 178 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 179 | @echo "Running Texinfo files through makeinfo..." 180 | make -C $(BUILDDIR)/texinfo info 181 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 182 | 183 | .PHONY: gettext 184 | gettext: 185 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 186 | @echo 187 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 188 | 189 | .PHONY: changes 190 | changes: 191 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 192 | @echo 193 | @echo "The overview file is in $(BUILDDIR)/changes." 194 | 195 | .PHONY: linkcheck 196 | linkcheck: 197 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 198 | @echo 199 | @echo "Link check complete; look for any errors in the above output " \ 200 | "or in $(BUILDDIR)/linkcheck/output.txt." 201 | 202 | .PHONY: doctest 203 | doctest: 204 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 205 | @echo "Testing of doctests in the sources finished, look at the " \ 206 | "results in $(BUILDDIR)/doctest/output.txt." 207 | 208 | .PHONY: coverage 209 | coverage: 210 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 211 | @echo "Testing of coverage in the sources finished, look at the " \ 212 | "results in $(BUILDDIR)/coverage/python.txt." 213 | 214 | .PHONY: xml 215 | xml: 216 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 217 | @echo 218 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 219 | 220 | .PHONY: pseudoxml 221 | pseudoxml: 222 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 223 | @echo 224 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 225 | 226 | .PHONY: dummy 227 | dummy: 228 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy 229 | @echo 230 | @echo "Build finished. Dummy builder generates no files." 231 | -------------------------------------------------------------------------------- /documentation/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | set I18NSPHINXOPTS=%SPHINXOPTS% source 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. epub3 to make an epub3 31 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 32 | echo. text to make text files 33 | echo. man to make manual pages 34 | echo. texinfo to make Texinfo files 35 | echo. gettext to make PO message catalogs 36 | echo. changes to make an overview over all changed/added/deprecated items 37 | echo. xml to make Docutils-native XML files 38 | echo. pseudoxml to make pseudoxml-XML files for display purposes 39 | echo. linkcheck to check all external links for integrity 40 | echo. doctest to run all doctests embedded in the documentation if enabled 41 | echo. coverage to run coverage check of the documentation if enabled 42 | echo. dummy to check syntax errors of document sources 43 | goto end 44 | ) 45 | 46 | if "%1" == "clean" ( 47 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 48 | del /q /s %BUILDDIR%\* 49 | goto end 50 | ) 51 | 52 | 53 | REM Check if sphinx-build is available and fallback to Python version if any 54 | %SPHINXBUILD% 1>NUL 2>NUL 55 | if errorlevel 9009 goto sphinx_python 56 | goto sphinx_ok 57 | 58 | :sphinx_python 59 | 60 | set SPHINXBUILD=python -m sphinx.__init__ 61 | %SPHINXBUILD% 2> nul 62 | if errorlevel 9009 ( 63 | echo. 64 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 65 | echo.installed, then set the SPHINXBUILD environment variable to point 66 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 67 | echo.may add the Sphinx directory to PATH. 68 | echo. 69 | echo.If you don't have Sphinx installed, grab it from 70 | echo.http://sphinx-doc.org/ 71 | exit /b 1 72 | ) 73 | 74 | :sphinx_ok 75 | 76 | 77 | if "%1" == "html" ( 78 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 79 | if errorlevel 1 exit /b 1 80 | echo. 81 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 82 | goto end 83 | ) 84 | 85 | if "%1" == "dirhtml" ( 86 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 87 | if errorlevel 1 exit /b 1 88 | echo. 89 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 90 | goto end 91 | ) 92 | 93 | if "%1" == "singlehtml" ( 94 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 95 | if errorlevel 1 exit /b 1 96 | echo. 97 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 98 | goto end 99 | ) 100 | 101 | if "%1" == "pickle" ( 102 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 103 | if errorlevel 1 exit /b 1 104 | echo. 105 | echo.Build finished; now you can process the pickle files. 106 | goto end 107 | ) 108 | 109 | if "%1" == "json" ( 110 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 111 | if errorlevel 1 exit /b 1 112 | echo. 113 | echo.Build finished; now you can process the JSON files. 114 | goto end 115 | ) 116 | 117 | if "%1" == "htmlhelp" ( 118 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 119 | if errorlevel 1 exit /b 1 120 | echo. 121 | echo.Build finished; now you can run HTML Help Workshop with the ^ 122 | .hhp project file in %BUILDDIR%/htmlhelp. 123 | goto end 124 | ) 125 | 126 | if "%1" == "qthelp" ( 127 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 128 | if errorlevel 1 exit /b 1 129 | echo. 130 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 131 | .qhcp project file in %BUILDDIR%/qthelp, like this: 132 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\gossip-python.qhcp 133 | echo.To view the help file: 134 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\gossip-python.ghc 135 | goto end 136 | ) 137 | 138 | if "%1" == "devhelp" ( 139 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 140 | if errorlevel 1 exit /b 1 141 | echo. 142 | echo.Build finished. 143 | goto end 144 | ) 145 | 146 | if "%1" == "epub" ( 147 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 148 | if errorlevel 1 exit /b 1 149 | echo. 150 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 151 | goto end 152 | ) 153 | 154 | if "%1" == "epub3" ( 155 | %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 156 | if errorlevel 1 exit /b 1 157 | echo. 158 | echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. 159 | goto end 160 | ) 161 | 162 | if "%1" == "latex" ( 163 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 164 | if errorlevel 1 exit /b 1 165 | echo. 166 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdf" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "latexpdfja" ( 181 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 182 | cd %BUILDDIR%/latex 183 | make all-pdf-ja 184 | cd %~dp0 185 | echo. 186 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 187 | goto end 188 | ) 189 | 190 | if "%1" == "text" ( 191 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 192 | if errorlevel 1 exit /b 1 193 | echo. 194 | echo.Build finished. The text files are in %BUILDDIR%/text. 195 | goto end 196 | ) 197 | 198 | if "%1" == "man" ( 199 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 200 | if errorlevel 1 exit /b 1 201 | echo. 202 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 203 | goto end 204 | ) 205 | 206 | if "%1" == "texinfo" ( 207 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 208 | if errorlevel 1 exit /b 1 209 | echo. 210 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 211 | goto end 212 | ) 213 | 214 | if "%1" == "gettext" ( 215 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 216 | if errorlevel 1 exit /b 1 217 | echo. 218 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 219 | goto end 220 | ) 221 | 222 | if "%1" == "changes" ( 223 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 224 | if errorlevel 1 exit /b 1 225 | echo. 226 | echo.The overview file is in %BUILDDIR%/changes. 227 | goto end 228 | ) 229 | 230 | if "%1" == "linkcheck" ( 231 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 232 | if errorlevel 1 exit /b 1 233 | echo. 234 | echo.Link check complete; look for any errors in the above output ^ 235 | or in %BUILDDIR%/linkcheck/output.txt. 236 | goto end 237 | ) 238 | 239 | if "%1" == "doctest" ( 240 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 241 | if errorlevel 1 exit /b 1 242 | echo. 243 | echo.Testing of doctests in the sources finished, look at the ^ 244 | results in %BUILDDIR%/doctest/output.txt. 245 | goto end 246 | ) 247 | 248 | if "%1" == "coverage" ( 249 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 250 | if errorlevel 1 exit /b 1 251 | echo. 252 | echo.Testing of coverage in the sources finished, look at the ^ 253 | results in %BUILDDIR%/coverage/python.txt. 254 | goto end 255 | ) 256 | 257 | if "%1" == "xml" ( 258 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 259 | if errorlevel 1 exit /b 1 260 | echo. 261 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 262 | goto end 263 | ) 264 | 265 | if "%1" == "pseudoxml" ( 266 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 267 | if errorlevel 1 exit /b 1 268 | echo. 269 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 270 | goto end 271 | ) 272 | 273 | if "%1" == "dummy" ( 274 | %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy 275 | if errorlevel 1 exit /b 1 276 | echo. 277 | echo.Build finished. Dummy builder generates no files. 278 | goto end 279 | ) 280 | 281 | :end 282 | -------------------------------------------------------------------------------- /documentation/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # gossip-python documentation build configuration file, created by 4 | # sphinx-quickstart on Sun May 15 14:05:57 2016. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | sys.path.insert(0, os.path.abspath('../../')) 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | #sys.path.insert(0, os.path.abspath('.')) 22 | 23 | # -- General configuration ------------------------------------------------ 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 30 | # ones. 31 | extensions = [ 32 | 'sphinx.ext.autodoc', 33 | 'sphinx.ext.doctest', 34 | 'sphinx.ext.mathjax', 35 | 'sphinx.ext.ifconfig', 36 | 'sphinx.ext.viewcode', 37 | 'sphinx.ext.githubpages', 38 | ] 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ['_templates'] 42 | 43 | # The suffix(es) of source filenames. 44 | # You can specify multiple suffix as a list of string: 45 | # source_suffix = ['.rst', '.md'] 46 | source_suffix = '.rst' 47 | 48 | # The encoding of source files. 49 | #source_encoding = 'utf-8-sig' 50 | 51 | # The master toctree document. 52 | master_doc = 'index' 53 | 54 | # General information about the project. 55 | project = u'gossip-python' 56 | copyright = u'2016, Anselm Binninger, Ralph Oliver Schaumann, Thomas Maier' 57 | author = u'Anselm Binninger, Ralph Oliver Schaumann, Thomas Maier' 58 | 59 | # The version info for the project you're documenting, acts as replacement for 60 | # |version| and |release|, also used in various other places throughout the 61 | # built documents. 62 | # 63 | # The short X.Y version. 64 | version = u'0.0.1' 65 | # The full version, including alpha/beta/rc tags. 66 | release = u'0.0.1' 67 | 68 | # The language for content autogenerated by Sphinx. Refer to documentation 69 | # for a list of supported languages. 70 | # 71 | # This is also used if you do content translation via gettext catalogs. 72 | # Usually you set "language" from the command line for these cases. 73 | language = None 74 | 75 | # There are two options for replacing |today|: either, you set today to some 76 | # non-false value, then it is used: 77 | #today = '' 78 | # Else, today_fmt is used as the format for a strftime call. 79 | #today_fmt = '%B %d, %Y' 80 | 81 | # List of patterns, relative to source directory, that match files and 82 | # directories to ignore when looking for source files. 83 | # This patterns also effect to html_static_path and html_extra_path 84 | exclude_patterns = ['examples', 'tests'] 85 | 86 | # The reST default role (used for this markup: `text`) to use for all 87 | # documents. 88 | #default_role = None 89 | 90 | # If true, '()' will be appended to :func: etc. cross-reference text. 91 | #add_function_parentheses = True 92 | 93 | # If true, the current module name will be prepended to all description 94 | # unit titles (such as .. function::). 95 | #add_module_names = True 96 | 97 | # If true, sectionauthor and moduleauthor directives will be shown in the 98 | # output. They are ignored by default. 99 | #show_authors = False 100 | 101 | 102 | autoclass_content = 'both' 103 | 104 | # The name of the Pygments (syntax highlighting) style to use. 105 | pygments_style = 'sphinx' 106 | 107 | # A list of ignored prefixes for module index sorting. 108 | #modindex_common_prefix = [] 109 | 110 | # If true, keep warnings as "system message" paragraphs in the built documents. 111 | #keep_warnings = False 112 | 113 | # If true, `todo` and `todoList` produce output, else they produce nothing. 114 | todo_include_todos = False 115 | 116 | autosummary_generate = True 117 | 118 | # -- Options for HTML output ---------------------------------------------- 119 | 120 | # The theme to use for HTML and HTML Help pages. See the documentation for 121 | # a list of builtin themes. 122 | html_theme = 'sphinx_rtd_theme' 123 | 124 | # Theme options are theme-specific and customize the look and feel of a theme 125 | # further. For a list of options available for each theme, see the 126 | # documentation. 127 | #html_theme_options = {} 128 | 129 | # Add any paths that contain custom themes here, relative to this directory. 130 | #html_theme_path = [] 131 | 132 | # The name for this set of Sphinx documents. 133 | # " v documentation" by default. 134 | #html_title = u'gossip-python v0.0.1' 135 | 136 | # A shorter title for the navigation bar. Default is the same as html_title. 137 | #html_short_title = None 138 | 139 | # The name of an image file (relative to this directory) to place at the top 140 | # of the sidebar. 141 | #html_logo = None 142 | 143 | # The name of an image file (relative to this directory) to use as a favicon of 144 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 145 | # pixels large. 146 | #html_favicon = None 147 | 148 | # Add any paths that contain custom static files (such as style sheets) here, 149 | # relative to this directory. They are copied after the builtin static files, 150 | # so a file named "default.css" will overwrite the builtin "default.css". 151 | html_static_path = ['_static'] 152 | 153 | # Add any extra paths that contain custom files (such as robots.txt or 154 | # .htaccess) here, relative to this directory. These files are copied 155 | # directly to the root of the documentation. 156 | #html_extra_path = [] 157 | 158 | # If not None, a 'Last updated on:' timestamp is inserted at every page 159 | # bottom, using the given strftime format. 160 | # The empty string is equivalent to '%b %d, %Y'. 161 | #html_last_updated_fmt = None 162 | 163 | # If true, SmartyPants will be used to convert quotes and dashes to 164 | # typographically correct entities. 165 | #html_use_smartypants = True 166 | 167 | # Custom sidebar templates, maps document names to template names. 168 | #html_sidebars = {} 169 | 170 | # Additional templates that should be rendered to pages, maps page names to 171 | # template names. 172 | #html_additional_pages = {} 173 | 174 | # If false, no module index is generated. 175 | #html_domain_indices = True 176 | 177 | # If false, no index is generated. 178 | #html_use_index = True 179 | 180 | # If true, the index is split into individual pages for each letter. 181 | #html_split_index = False 182 | 183 | # If true, links to the reST sources are added to the pages. 184 | #html_show_sourcelink = True 185 | 186 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 187 | #html_show_sphinx = True 188 | 189 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 190 | #html_show_copyright = True 191 | 192 | # If true, an OpenSearch description file will be output, and all pages will 193 | # contain a tag referring to it. The value of this option must be the 194 | # base URL from which the finished HTML is served. 195 | #html_use_opensearch = '' 196 | 197 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 198 | #html_file_suffix = None 199 | 200 | # Language to be used for generating the HTML full-text search index. 201 | # Sphinx supports the following languages: 202 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 203 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' 204 | #html_search_language = 'en' 205 | 206 | # A dictionary with options for the search language support, empty by default. 207 | # 'ja' uses this config value. 208 | # 'zh' user can custom change `jieba` dictionary path. 209 | #html_search_options = {'type': 'default'} 210 | 211 | # The name of a javascript file (relative to the configuration directory) that 212 | # implements a search results scorer. If empty, the default will be used. 213 | #html_search_scorer = 'scorer.js' 214 | 215 | # Output file base name for HTML help builder. 216 | htmlhelp_basename = 'gossip-pythondoc' 217 | 218 | # -- Options for LaTeX output --------------------------------------------- 219 | 220 | latex_elements = { 221 | # The paper size ('letterpaper' or 'a4paper'). 222 | #'papersize': 'letterpaper', 223 | 224 | # The font size ('10pt', '11pt' or '12pt'). 225 | #'pointsize': '10pt', 226 | 227 | # Additional stuff for the LaTeX preamble. 228 | #'preamble': '', 229 | 230 | # Latex figure (float) alignment 231 | #'figure_align': 'htbp', 232 | } 233 | 234 | # Grouping the document tree into LaTeX files. List of tuples 235 | # (source start file, target name, title, 236 | # author, documentclass [howto, manual, or own class]). 237 | latex_documents = [ 238 | (master_doc, 'gossip-python.tex', u'gossip-python Documentation', 239 | u'Anselm Binninger, Ralph Oliver Schaumann, Thomas Maier', 'manual'), 240 | ] 241 | 242 | # The name of an image file (relative to this directory) to place at the top of 243 | # the title page. 244 | #latex_logo = None 245 | 246 | # For "manual" documents, if this is true, then toplevel headings are parts, 247 | # not chapters. 248 | #latex_use_parts = False 249 | 250 | # If true, show page references after internal links. 251 | #latex_show_pagerefs = False 252 | 253 | # If true, show URL addresses after external links. 254 | #latex_show_urls = False 255 | 256 | # Documents to append as an appendix to all manuals. 257 | #latex_appendices = [] 258 | 259 | # If false, no module index is generated. 260 | #latex_domain_indices = True 261 | 262 | 263 | # -- Options for manual page output --------------------------------------- 264 | 265 | # One entry per manual page. List of tuples 266 | # (source start file, name, description, authors, manual section). 267 | man_pages = [ 268 | (master_doc, 'gossip-python', u'gossip-python Documentation', 269 | [author], 1) 270 | ] 271 | 272 | # If true, show URL addresses after external links. 273 | #man_show_urls = False 274 | 275 | 276 | # -- Options for Texinfo output ------------------------------------------- 277 | 278 | # Grouping the document tree into Texinfo files. List of tuples 279 | # (source start file, target name, title, author, 280 | # dir menu entry, description, category) 281 | texinfo_documents = [ 282 | (master_doc, 'gossip-python', u'gossip-python Documentation', 283 | author, 'gossip-python', 'One line description of project.', 284 | 'Miscellaneous'), 285 | ] 286 | 287 | # Documents to append as an appendix to all manuals. 288 | #texinfo_appendices = [] 289 | 290 | # If false, no module index is generated. 291 | #texinfo_domain_indices = True 292 | 293 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 294 | #texinfo_show_urls = 'footnote' 295 | 296 | # If true, do not generate a @detailmenu in the "Top" node's menu. 297 | #texinfo_no_detailmenu = False 298 | -------------------------------------------------------------------------------- /documentation/source/index.rst: -------------------------------------------------------------------------------- 1 | .. gossip documentation master file, created by 2 | sphinx-quickstart on Wed Apr 27 19:34:41 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to gossip's documentation! 7 | ================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | .. automodule:: gossip 14 | gossip 15 | :maxdepth: 2 16 | 17 | 18 | Indices and tables 19 | ================== 20 | 21 | * :ref:`genindex` 22 | * :ref:`modindex` 23 | * :ref:`search` 24 | 25 | Getting Started 26 | ================== 27 | 28 | Other 29 | ################## 30 | 31 | 32 | #. You can download this documentation file from: `gossip-python.pdf `_ 33 | #. The current pylint output can be found here `pylint-gossip `_ 34 | #. The current test outputs can be found here `gossip-test ouput `_ 35 | 36 | Installation 37 | ################### 38 | 39 | #. Download gossip from `Downloads `_ 40 | #. Install gossip 41 | .. code-block:: sh 42 | 43 | pip install ./gossip-0.0.1.tar.gz 44 | 45 | #. See Usage for more! 46 | 47 | 48 | Usage 49 | ################### 50 | 51 | Run gossip 52 | ___________________ 53 | 54 | * To start a new gossip instance on your machine, you first need to create a new directory called config at the same level of your python script. 55 | * Second, you need to add at least two files to that directory. The first file, called logging_config.ini is needed to specify the log level of this gossip instance 56 | 57 | .. code-block:: ini 58 | 59 | [loggers] 60 | keys=root 61 | 62 | [handlers] 63 | keys=consoleHandler 64 | 65 | [formatters] 66 | keys=simpleFormatter 67 | 68 | [logger_root] 69 | level=DEBUG 70 | handlers=consoleHandler 71 | 72 | [handler_consoleHandler] 73 | class=StreamHandler 74 | level=DEBUG # Replace this with the log level you want, for production we recommend ERROR 75 | formatter=simpleFormatter 76 | args=(sys.stdout,) 77 | 78 | [formatter_simpleFormatter] 79 | format=%(asctime)s - %(name)s - %(levelname)s - %(message)s 80 | datefmt=%d.%m.%Y %H:%M:%S 81 | 82 | 83 | * Third, create a file called config.ini : 84 | 85 | .. code-block:: ini 86 | 87 | [GOSSIP] 88 | # Max number of messages this peer can cache 89 | cache_size = 50 90 | # Max number of peer connections this peer can hold 91 | max_connections = 30 92 | # The bootstrapping gossip instance (leave empty if you want to act as the bootstrapper) 93 | bootstrapper = 192.168.1.100:6001 94 | # The address this machine listens for peer connections 95 | listen_address = 192.168.1.99:6001 96 | # The address this machine listens for api connections 97 | api_address = 192.168.2.99:7001 98 | # Used for messages that are sent through the api 99 | max_ttl = 0 100 | 101 | Note that you should replace the listen_address and api_address with the ip address of your machine. 102 | If you want your machine to be the bootstrapping machine, leave bootstrapper empty. If not replace this with 103 | the list_address of the machine you want to use as the bootstrapper. eg (192.168.1.100:6001) 104 | For an example of a api application see this repository: `ChatNow! - Repository `_ 105 | If you want to test your gossip network you can download the latest ChatNow! 106 | Client from `ChatNow! - Downloads `_ 107 | 108 | * Once you created these files you can create a python script like this to run your gossip instance 109 | 110 | .. code-block:: python 111 | 112 | from gossip.gossip_main import main as run_gossip 113 | 114 | if __name__ == "__main__": 115 | run_gossip() 116 | 117 | 118 | Run simulation 119 | ___________________ 120 | 121 | The simulation script generates a network based on the same algorithms that are used in gossip-python. 122 | This can especially useful if you want to see how the network behaves using different configuration paremeters. 123 | Note that you need to install the following requirements yourself to get the simulation running: 124 | .. code-block:: sh 125 | 126 | pip install networkx 127 | pip install matplotlib 128 | 129 | To run the simulation, which creates a network diagram, see following code. 130 | 131 | .. code-block:: python 132 | 133 | from examples.connections_simulation import Simulation 134 | 135 | if __name__ == "__main__": 136 | sim = Simulation(number_clients=30, number_connections_per_client=5) 137 | sim.run() -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | __author__ = 'Anselm Binninger, Ralph Schaumann, Thomas Maier' 16 | -------------------------------------------------------------------------------- /examples/connections_simulation.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import random 16 | import logging 17 | import logging.config 18 | import argparse 19 | import networkx 20 | from multiprocessing import Lock 21 | 22 | from gossip.communication.connection import GossipConnectionPool 23 | import matplotlib 24 | 25 | matplotlib.use('PDF') 26 | import matplotlib.pyplot as plt 27 | 28 | __author__ = 'Anselm Binninger, Ralph Oliver Schaumann, Thomas Maier' 29 | 30 | import sys 31 | 32 | 33 | # Print iterations progress 34 | def print_progress(iteration, total, prefix='', suffix='', decimals=1, bar_length=100): 35 | """ 36 | Call in a loop to create terminal progress bar 37 | @params: 38 | iteration - Required : current iteration (Int) 39 | total - Required : total iterations (Int) 40 | prefix - Optional : prefix string (Str) 41 | suffix - Optional : suffix string (Str) 42 | decimals - Optional : number of decimals in percent complete (Int) 43 | bar_length - Optional : character length of bar (Int) 44 | """ 45 | filled_length = int(round(bar_length * iteration / float(total)) / 2) 46 | percents = round(100.00 * (iteration / float(total)), decimals) 47 | bar = u'\u2588' * filled_length + u'\u2591' * int(bar_length / 2 - filled_length) 48 | sys.stdout.write('\r%s |%s| %s%s %s' % (prefix, bar, percents, '%', suffix)), 49 | sys.stdout.flush() 50 | if iteration == total: 51 | sys.stdout.write('\n') 52 | sys.stdout.flush() 53 | 54 | 55 | class GossipConnectionPoolFast(GossipConnectionPool): 56 | 57 | def __init__(self, connection_pool_label, cache_size=30): 58 | """Constructor.""" 59 | self.connection_pool_label = connection_pool_label 60 | self._connections = {} 61 | self._cache_size = cache_size 62 | self._pool_lock = Lock() 63 | 64 | class Client: 65 | """Dummy class that mocks a client""" 66 | 67 | def __init__(self, ip, port, bootstrapper, max_chache_size=30): 68 | self.ip = ip 69 | self.port = port 70 | self.max_cache_size = max_chache_size 71 | self.bootstrapper = bootstrapper 72 | self._connection_pool = GossipConnectionPoolFast(connection_pool_label="%s:%s" % (ip, port), 73 | cache_size=self.max_cache_size) 74 | self._connection_pool._connections = {} 75 | 76 | def get_ident(self): 77 | return '%s:%s' % (self.ip, self.port) 78 | 79 | def add_connection(self, connection): 80 | if connection.get_client(self).get_ident() != self.get_ident(): 81 | self._connection_pool.add_connection(connection=connection, 82 | identifier=connection.get_client(self).get_ident()) 83 | 84 | def get_connection_idents(self): 85 | return self._connection_pool.get_identifiers() 86 | 87 | def get_connections(self): 88 | return [self._connection_pool.get_connection(ident) for ident in self._connection_pool.get_identifiers()] 89 | 90 | def notify(self, other_client): 91 | self._connection_pool.remove_connection(other_client.get_ident()) 92 | if len(self._connection_pool.get_identifiers()) < self.max_cache_size - 1: 93 | logging.info("%s - Pool not full!!!!" % self.get_ident()) 94 | other_connections = [] 95 | connections = self.get_connections() + self.bootstrapper.get_connections() if self.bootstrapper else [] 96 | for connection in connections: 97 | other_client = connection.second_client 98 | other_clients_connections = other_client.get_connections() 99 | for other_connection in other_clients_connections: 100 | if other_connection.second_client.get_ident() != self.get_ident() and \ 101 | len( 102 | other_connection.second_client.get_connection_idents()) < self.max_cache_size - 1: 103 | other_connections.append(other_connection) 104 | if len(other_connections) > 0: 105 | random_connection = other_connections[random.randint(0, len(other_connections) - 1)] 106 | logging.debug("Pool not full, connection to random new client: %s", 107 | random_connection.second_client.get_ident()) 108 | new_connection = Connection(self, random_connection.second_client) 109 | new_connection.initiate() 110 | 111 | 112 | class Connection: 113 | def __init__(self, first_client, second_client): 114 | self.first_client = first_client 115 | self.second_client = second_client 116 | 117 | def initiate(self): 118 | self.first_client.add_connection(self) 119 | self.second_client.add_connection(Connection(self.second_client, self.first_client)) 120 | 121 | def close(self): 122 | self.first_client.notify(self.second_client) 123 | self.second_client.notify(self.first_client) 124 | 125 | def shutdown(self,param): 126 | pass 127 | 128 | def get_client(self, client): 129 | if client == self.second_client: 130 | return self.first_client 131 | else: 132 | return self.second_client 133 | 134 | def __str__(self): 135 | return "%s -> %s" % (self.first_client.get_ident(), self.second_client.get_ident()) 136 | 137 | def __repr__(self): 138 | return self.__str__() 139 | 140 | 141 | class Simulation: 142 | def __init__(self, number_clients=1000, number_connections_per_client=30): 143 | print("Running simulation with %s peers and %s connections per peer" % ( 144 | number_clients, number_connections_per_client)) 145 | self._number_clients = number_clients 146 | self._number_connections_per_client = number_connections_per_client 147 | 148 | def run(self): 149 | ip_addresses = ['192.168.1.%s' % x for x in range(1, self._number_clients + 1)] 150 | ports = [x for x in range(1, 2)] 151 | clients = [] 152 | progress = 0 153 | for ip_addr in ip_addresses: 154 | progress += 1 155 | print_progress(progress, self._number_clients, suffix="Running simulation") 156 | for port in ports: 157 | client = Client(ip_addr, port, clients[0] if len(clients) > 0 else None, 158 | max_chache_size=self._number_connections_per_client) 159 | clients.append(client) 160 | connection = Connection(client, clients[0]) 161 | connection.initiate() 162 | 163 | bootstrapper_connections = clients[0].get_connections() 164 | for conn in bootstrapper_connections: 165 | connection = Connection(client, conn.second_client) 166 | connection.initiate() 167 | 168 | graph = networkx.nx.Graph() 169 | for client in clients: 170 | logging.info(client.get_ident()) 171 | logging.info(client.get_connection_idents()) 172 | for node in client.get_connections(): 173 | graph.add_edge(node.first_client.get_ident(), node.second_client.get_ident()) 174 | 175 | networkx.draw(graph, with_labels=False) 176 | plt.savefig("path_graph.pdf") 177 | print("Network is connected: %s" % networkx.is_connected(graph)) 178 | print("Average shortest path length: %s" % networkx.average_shortest_path_length(graph)) 179 | print("Average bipartite clustering coefficient %s" % networkx.average_clustering(graph)) 180 | print("Bipartite clustering coefficient %s" % networkx.clustering(graph)) 181 | print("degree_assortativity_coefficient %s" % networkx.degree_assortativity_coefficient(graph)) 182 | 183 | 184 | parser = argparse.ArgumentParser(description='Run gossip simulation') 185 | parser.add_argument('-n', dest='peers', type=int, default=50, 186 | help='Number of clients to run the simulation with') 187 | parser.add_argument('-c', dest='connections', type=int, default=6, 188 | help='Number of connections allowed per client') 189 | 190 | if __name__ == '__main__': 191 | args = parser.parse_args() 192 | sim = Simulation(number_clients=args.peers, number_connections_per_client=args.connections) 193 | sim.run() 194 | print("\n################################################################\n") 195 | print("INFO: Network diagram saved to current file directory (path_graph.pdf)") 196 | -------------------------------------------------------------------------------- /examples/peer_request_response.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import socket 16 | 17 | from gossip.util import packing 18 | from gossip.util import message 19 | 20 | __author__ = 'Anselm Binninger, Ralph Schaumann, Thomas Maier' 21 | 22 | sock1 = socket.socket() 23 | sock2 = socket.socket() 24 | sock1.connect(('localhost', 6001)) 25 | sock2.connect(('localhost', 6001)) 26 | values = packing.pack_gossip_peer_request() 27 | packing.send_msg(sock1, values['code'], values['data']) 28 | values = packing.receive_msg(sock1) 29 | print(values) 30 | message_object = message.MessageGossipPeerResponse(values['message']) 31 | value = message_object.get_values() 32 | print(message_object.connections) 33 | sock1.close() 34 | sock2.close() 35 | -------------------------------------------------------------------------------- /examples/receive_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import socket 16 | from gossip.util import packing, message 17 | 18 | __author__ = 'Anselm Binninger, Ralph Schaumann, Thomas Maier' 19 | 20 | try: 21 | while True: 22 | sock = socket.socket() 23 | sock.connect(('localhost', 6001)) 24 | values = packing.receive_msg(sock) 25 | message_object = message.GOSSIP_MESSAGE_TYPES.get(values['code'], message.MessageOther) 26 | if 500 <= values['code'] < 520: 27 | print(message_object(values['message'])) 28 | else: 29 | print(values) 30 | sock.close() 31 | except Exception as e: 32 | print('%s' % e) 33 | -------------------------------------------------------------------------------- /examples/send_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import socket 16 | from gossip.util import packing 17 | 18 | import time 19 | 20 | __author__ = 'Anselm Binninger, Ralph Schaumann, Thomas Maier' 21 | 22 | try: 23 | 24 | port = 6001 25 | ip_address = 'localhost' 26 | 27 | print('sending garbage') 28 | sock = socket.socket() 29 | sock.connect((ip_address, port)) 30 | sock.send(bytes('s', 'ascii')) 31 | sock.close() 32 | print('garbage sent') 33 | time.sleep(2) 34 | 35 | print('Sending 404') 36 | sock = socket.socket() 37 | sock.connect((ip_address, port)) 38 | values = packing.pack_message_other(404, 'message not found') 39 | packing.send_msg(sock, values['code'], values['data']) 40 | sock.close() 41 | print('404 sent') 42 | time.sleep(2) 43 | 44 | print('Sending 500') 45 | sock = socket.socket() 46 | sock.connect((ip_address, port)) 47 | values = packing.pack_gossip_announce(0, 540, 'p2p is very cool!') 48 | packing.send_msg(sock, values['code'], values['data']) 49 | sock.close() 50 | print('500 sent') 51 | time.sleep(2) 52 | 53 | print('Sending 501') 54 | sock = socket.socket() 55 | sock.connect((ip_address, port)) 56 | values = packing.pack_gossip_notify(540) 57 | packing.send_msg(sock, values['code'], values['data']) 58 | sock.close() 59 | print('501 sent') 60 | time.sleep(2) 61 | 62 | print('Sending 502') 63 | sock = socket.socket() 64 | sock.connect((ip_address, port)) 65 | values = packing.pack_gossip_notification(1, 540, 'p2p is very cool!') 66 | packing.send_msg(sock, values['code'], values['data']) 67 | sock.close() 68 | print('502 sent') 69 | time.sleep(2) 70 | 71 | print('Sending 503') 72 | sock = socket.socket() 73 | sock.connect((ip_address, port)) 74 | values = packing.pack_gossip_validation(1, 1) 75 | packing.send_msg(sock, values['code'], values['data']) 76 | sock.close() 77 | print('503 sent') 78 | time.sleep(2) 79 | 80 | print('Sending 510') 81 | sock = socket.socket() 82 | sock.connect((ip_address, port)) 83 | values = packing.pack_gossip_peer_request() 84 | packing.send_msg(sock, values['code'], values['data']) 85 | sock.close() 86 | print('510 sent') 87 | time.sleep(2) 88 | 89 | print('Sending 511') 90 | sock = socket.socket() 91 | values = packing.pack_gossip_peer_response({'1.1.1.1:1': None, '2.2.2.2:2': None, '3.3.3.3:3': None}) 92 | sock.connect((ip_address, port)) 93 | packing.send_msg(sock, values['code'], values['data']) 94 | sock.close() 95 | print('511 sent') 96 | time.sleep(2) 97 | 98 | print('Sending 512') 99 | sock = socket.socket() 100 | sock.connect((ip_address, port)) 101 | values = packing.pack_gossip_peer_update('127.0.0.1', 2222, 0, 1) 102 | packing.send_msg(sock, values['code'], values['data']) 103 | sock.close() 104 | print('512 sent') 105 | 106 | except Exception as e: 107 | print("connection error: {0}".format(e)) 108 | -------------------------------------------------------------------------------- /gossip/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | Main application module. 17 | 18 | It consists of three submodules, api, control and transfer 19 | 20 | 21 | api - Entrypoint for incoming application calls 22 | 23 | control - Handles the incoming application calls 24 | 25 | transfer - Either notifies other gossip instances on remote machines or handles incoming gossip calls (from remote) 26 | """ 27 | 28 | __author__ = 'Anselm Binninger, Thomas Maier, Ralph Schaumann' 29 | -------------------------------------------------------------------------------- /gossip/communication/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | Submodule which contains communication functionalities for the P2P and API layer of Gossip. 17 | """ 18 | 19 | __author__ = 'Anselm Binninger, Thomas Maier, Ralph Schaumann' 20 | -------------------------------------------------------------------------------- /gossip/communication/client_receiver.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import logging 16 | from multiprocessing import Process 17 | 18 | from gossip.util import packing 19 | from gossip.util.exceptions import GossipMessageException, GossipClientDisconnectedException, \ 20 | GossipMessageFormatException 21 | from gossip.util.message import MessageOther 22 | from gossip.util.message import GOSSIP_MESSAGE_TYPES 23 | from gossip.util.queue_item_types import QUEUE_ITEM_TYPE_RECEIVED_MESSAGE, QUEUE_ITEM_TYPE_CONNECTION_LOST, \ 24 | QUEUE_ITEM_TYPE_NEW_CONNECTION 25 | 26 | __author__ = 'Anselm Binninger, Thomas Maier, Ralph Schaumann' 27 | 28 | 29 | class GossipClientReceiver(Process): 30 | """ A client receiver is a process which receives data from a specified socket. """ 31 | def __init__(self, client_receiver_label, client_socket, ipv4_address, tcp_port, to_controller_queue, 32 | connection_pool): 33 | """ Constructor. 34 | 35 | :param client_receiver_label: A label to derive the concrete functionality of this client receiver 36 | :param client_socket: The socket from/to the affected the affected client 37 | :param ipv4_address: The IPv4 address of the client 38 | :param tcp_port: The TCP port of the client 39 | :param to_controller_queue: The queue which connects this client receiver with the responsible controller 40 | :param connection_pool: If the socket crashes, the connection will be removed in this connection pool 41 | """ 42 | Process.__init__(self) 43 | self.client_receiver_label = client_receiver_label 44 | self.client_socket = client_socket 45 | self.identifier = '%s:%d' % (ipv4_address, tcp_port) 46 | self.to_controller_queue = to_controller_queue 47 | self.connection_pool = connection_pool 48 | 49 | def run(self): 50 | """ This typical run method of the client receiver process is responsible for handling a connection for 51 | the Gossip instance. It handles incoming messages and forwards them to the controller. If a connection crashes, 52 | this method pushes a specified command to the responsible controller. """ 53 | logging.info('%s (%s) started' % (self.client_receiver_label, self.identifier)) 54 | try: 55 | self.handle_client() 56 | except GossipClientDisconnectedException: 57 | logging.info('%s (%s) Removing connection from connection pool' % (self.client_receiver_label, 58 | self.identifier)) 59 | self.connection_pool.remove_connection(self.identifier) 60 | self.to_controller_queue.put({'type': QUEUE_ITEM_TYPE_CONNECTION_LOST, 61 | 'identifier': self.identifier, 62 | 'message': None}) 63 | 64 | def handle_client(self): 65 | """ Receives new messages until the client dies. It also kills connections to clients which send malformed 66 | messages. Therefor it informs the responsible controller as well. """ 67 | self.to_controller_queue.put({'type': QUEUE_ITEM_TYPE_NEW_CONNECTION, 68 | 'identifier': self.identifier, 69 | 'message': None}) 70 | try: 71 | while True: 72 | message = self.__receive() 73 | logging.debug('%s (%s) | Received message %s' % (self.client_receiver_label, self.identifier, message)) 74 | except (GossipMessageException, GossipMessageFormatException) as e: 75 | logging.debug('%s (%s) | Received undecodable or invalid message: %s' % (self.client_receiver_label, 76 | self.identifier, e)) 77 | raise GossipClientDisconnectedException('Lost %s (%s)' % (self.client_receiver_label, self.identifier)) 78 | except (ConnectionResetError, ConnectionAbortedError, GossipClientDisconnectedException): 79 | logging.debug('%s (%s) | Client disconnected' % (self.client_receiver_label, self.identifier)) 80 | raise GossipClientDisconnectedException('Lost %s (%s)' % (self.client_receiver_label, self.identifier)) 81 | 82 | self.client_socket.close() 83 | 84 | def __receive(self): 85 | """ 86 | Receives a new message, unpacks it and forwards it to the assigned controller. 87 | 88 | :returns: The received message object 89 | """ 90 | msg = packing.receive_msg(self.client_socket) 91 | if msg['code'] in GOSSIP_MESSAGE_TYPES.keys(): 92 | try: 93 | message_object = GOSSIP_MESSAGE_TYPES[msg['code']](msg['message']) 94 | except Exception as e: 95 | # TODO Don't catch Exception, catch specific decoding exception 96 | raise GossipMessageFormatException('%s' % e) 97 | else: 98 | message_object = MessageOther(msg['code'], msg['message']) 99 | 100 | self.to_controller_queue.put({'type': QUEUE_ITEM_TYPE_RECEIVED_MESSAGE, 101 | 'identifier': self.identifier, 102 | 'message': message_object}) 103 | return message_object 104 | -------------------------------------------------------------------------------- /gossip/communication/client_sender.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import logging 16 | import multiprocessing 17 | import socket 18 | 19 | from gossip.util.exceptions import GossipQueueException, GossipIdentifierNotFound 20 | from gossip.util.queue_item_types import QUEUE_ITEM_TYPE_SEND_MESSAGE, QUEUE_ITEM_TYPE_ESTABLISH_CONNECTION 21 | from gossip.communication.client_receiver import GossipClientReceiver 22 | 23 | __author__ = 'Anselm Binninger, Thomas Maier, Ralph Schaumann' 24 | 25 | 26 | class GossipSender(multiprocessing.Process): 27 | """ The Gossip sender receives new commands from the responsible controller. The sender is responsible for sending 28 | new messages to specified receivers. It is able to establish new connections as well if the controller sends the 29 | appropriate command to do so. """ 30 | 31 | def __init__(self, sender_label, from_controller_queue, to_controller_queue, connection_pool): 32 | """ Constructor. 33 | 34 | :param sender_label: A label to derive the concrete functionality of this client sender 35 | :param from_controller_queue: The client sender gets new commands via this queue from the responsible controller 36 | :param to_controller_queue: This instance forwards the controller queue to new receiver instances 37 | :param connection_pool: The connection pool which contains all connections/sockets 38 | """ 39 | multiprocessing.Process.__init__(self) 40 | self.sender_label = sender_label 41 | self.from_controller_queue = from_controller_queue 42 | self.to_controller_queue = to_controller_queue 43 | self.connection_pool = connection_pool 44 | 45 | def run(self): 46 | """ This is a typical run method for the sender process. It waits for commands from the controller to establish 47 | new connections or to send messages to established connections. The sender gets the appropriate 48 | connection/socket from the connection pool. """ 49 | logging.info('%s started - PID: %s' % (self.sender_label, self.pid)) 50 | 51 | while True: 52 | queue_item = self.from_controller_queue.get() 53 | queue_item_type = queue_item['type'] 54 | identifier = queue_item['identifier'] 55 | 56 | # Fetch the right connection 57 | if queue_item_type == QUEUE_ITEM_TYPE_SEND_MESSAGE: 58 | message = queue_item['message'] 59 | 60 | # Fetch connection from connection pool 61 | logging.info("%s | Redirecting message (code %d) to corresponding client" 62 | % (self.sender_label, message.get_values()['code'])) 63 | try: 64 | connection = self.connection_pool.get_connection(identifier) 65 | except GossipIdentifierNotFound: 66 | connection = None 67 | 68 | # Send message 69 | if connection: 70 | if message: 71 | encoded = message.encode() 72 | try: 73 | connection.send(encoded) 74 | logging.debug('%s | Sent message (%s) to client %s | Sent message: %s' 75 | % (self.sender_label, message.get_values()['code'], identifier, 76 | message.get_values())) 77 | except (ConnectionResetError, ConnectionAbortedError): 78 | self.connection_pool.remove_connection(identifier) 79 | logging.error('%s | During sending a message peer disconnected' % self.sender_label) 80 | else: 81 | logging.error('%s | No connection found in connection pool, giving up' % self.sender_label) 82 | 83 | elif queue_item_type == QUEUE_ITEM_TYPE_ESTABLISH_CONNECTION: 84 | # Establish new connection 85 | logging.info("%s Establishing new connection to %s" % (self.sender_label, identifier)) 86 | connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 87 | server_host, server_port = identifier.split(':') 88 | server_port = int(server_port) 89 | try: 90 | connection.connect((server_host, server_port)) 91 | self.connection_pool.add_connection(identifier, connection, server_identifier=identifier) 92 | except ConnectionRefusedError: 93 | logging.error('%s | Cannot establish connection to %s' % (self.sender_label, identifier)) 94 | continue 95 | 96 | logging.info("%s | Added new connection to connection pool" % self.sender_label) 97 | 98 | # Create client receiver for new connection 99 | # TODO Client receiver label should not be hardcoded here! 100 | client_receiver = GossipClientReceiver('P2PClientReceiver', connection, server_host, server_port, 101 | self.to_controller_queue, self.connection_pool) 102 | 103 | client_receiver.start() 104 | 105 | else: 106 | # If this happens, someone did a horrible mistake in the code: The queue item type is not supported! 107 | raise GossipQueueException('%s: Queue item cannot be identified! This should never happen!' 108 | % self.sender_label) 109 | -------------------------------------------------------------------------------- /gossip/communication/connection.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import logging 16 | import random 17 | from multiprocessing import Manager, Lock 18 | from socket import SHUT_RDWR 19 | from gossip.util.exceptions import GossipIdentifierNotFound 20 | 21 | __author__ = 'Anselm Binninger, Thomas Maier, Ralph Schaumann' 22 | 23 | 24 | class GossipConnectionPool: 25 | """ Thread-safe implementation of a pool for Gossip connections. """ 26 | CONNECTION = 'Connection' 27 | SERVER_IDENTIFIER = 'ServerIdentifier' 28 | 29 | def __init__(self, connection_pool_label, cache_size=30): 30 | """ Constructor. 31 | 32 | :param connection_pool_label: A label to derive the concrete functionality of this connection pool 33 | :param cache_size: (optional): The max. amount of connections in this connection pool. 34 | """ 35 | self.connection_pool_label = connection_pool_label 36 | self._connections = Manager().dict() 37 | self._cache_size = cache_size 38 | self._pool_lock = Lock() 39 | 40 | def add_connection(self, identifier, connection, server_identifier=None): 41 | """ Adds new identifier with its connection. 42 | 43 | :param identifier: An object which identifies an unique connection 44 | :param connection: A connection object 45 | :param server_identifier: (optional) The server identifier of the peer 46 | """ 47 | self._pool_lock.acquire() 48 | if identifier not in self._connections: 49 | self._connections[identifier] = {GossipConnectionPool.CONNECTION: connection, 50 | GossipConnectionPool.SERVER_IDENTIFIER: server_identifier} 51 | logging.debug('%s | Added new connection %s (pool: %s)' % (self.connection_pool_label, identifier, self)) 52 | self._pool_lock.release() 53 | self.__maintain_connections() 54 | else: 55 | self._pool_lock.release() 56 | logging.debug('%s | Connection %s exists already (pool: %s)' % (self.connection_pool_label, identifier, 57 | self)) 58 | 59 | def update_connection(self, identifier, server_identifier): 60 | """ Updates an existing identifier with its connection. 61 | 62 | :param identifier: An object which identifies an unique connection 63 | :param server_identifier: The server identifier of the peer 64 | """ 65 | self._pool_lock.acquire() 66 | if identifier in self._connections: 67 | connection_to_update = self._connections[identifier][GossipConnectionPool.CONNECTION] 68 | self._connections[identifier] = {GossipConnectionPool.CONNECTION: connection_to_update, 69 | GossipConnectionPool.SERVER_IDENTIFIER: server_identifier} 70 | logging.debug('%s | Updated information about connection %s (pool: %s)' % (self.connection_pool_label, 71 | identifier, self)) 72 | else: 73 | logging.debug('%s | Updating information about connection %s failed because it does not exist anymore' 74 | % (self.connection_pool_label, identifier)) 75 | self._pool_lock.release() 76 | 77 | def remove_connection(self, identifier): 78 | """ Removes an existing connection from the pool. 79 | 80 | :param identifier: Unique identifier to find the affected connection 81 | """ 82 | self._pool_lock.acquire() 83 | removed_connection = self._connections.pop(identifier, None) 84 | if removed_connection: 85 | logging.debug('%s | Removed connection %s (pool: %s)' % (self.connection_pool_label, identifier, self)) 86 | self._pool_lock.release() 87 | return removed_connection[GossipConnectionPool.CONNECTION] 88 | self._pool_lock.release() 89 | 90 | def get_connection(self, identifier): 91 | """ Gets a connection from the pool. 92 | 93 | :param identifier: Unique identifier to find the affected connection 94 | """ 95 | self._pool_lock.acquire() 96 | connection = self._connections.get(identifier, None) 97 | if connection: 98 | self._pool_lock.release() 99 | return connection[GossipConnectionPool.CONNECTION] 100 | else: 101 | self._pool_lock.release() 102 | raise GossipIdentifierNotFound('Cannot find identifier %s' % identifier) 103 | 104 | def get_server_identifier(self, identifier): 105 | """ Gets the server identifier for one connection. 106 | 107 | :param identifier: Unique identifier to find the affected server identifier 108 | """ 109 | self._pool_lock.acquire() 110 | connection = self._connections.get(identifier, None) 111 | if connection: 112 | self._pool_lock.release() 113 | return connection[GossipConnectionPool.SERVER_IDENTIFIER] 114 | else: 115 | self._pool_lock.release() 116 | raise GossipIdentifierNotFound('Cannot find identifier %s' % identifier) 117 | 118 | def get_identifiers(self): 119 | """ Gets a list of all identifiers. 120 | 121 | :returns: List of all identifier strings 122 | """ 123 | self._pool_lock.acquire() 124 | identifiers = self._connections.keys() 125 | self._pool_lock.release() 126 | return identifiers 127 | 128 | def get_server_identifiers(self, identifier_to_exclude=None): 129 | """ Collects server identifiers 130 | 131 | :param identifier_to_exclude: (optional) Server identifiers to exclude 132 | """ 133 | if not identifier_to_exclude: 134 | identifier_to_exclude = [] 135 | 136 | server_identifiers = [] 137 | self._pool_lock.acquire() 138 | for identifier, connection in self._connections.items(): 139 | server_address = connection[GossipConnectionPool.SERVER_IDENTIFIER] 140 | if server_address and server_address not in identifier_to_exclude: 141 | server_identifiers.append(server_address) 142 | self._pool_lock.release() 143 | return server_identifiers 144 | 145 | def __str__(self): 146 | output = ', '.join(['%s<=%s' % (key, val[GossipConnectionPool.SERVER_IDENTIFIER]) 147 | for key, val in self._connections.items()]) 148 | if output == '': 149 | output = 'Pool is empty' 150 | return output 151 | 152 | def __maintain_connections(self): 153 | """ Maintains the list of connections. If number of current connections exceeds maximum cache size a random 154 | connection is killed. """ 155 | if len(self._connections) > self._cache_size: 156 | self._pool_lock.acquire() 157 | connection_to_remove = list(self._connections.keys())[random.randint(0, len(self._connections) - 1)] 158 | self._pool_lock.release() 159 | killed_connection = self.remove_connection(connection_to_remove) 160 | killed_connection.shutdown(SHUT_RDWR) 161 | killed_connection.close() 162 | logging.debug('%s | Connection maintainer removes: %s (current pool: %s)' % (self.connection_pool_label, 163 | connection_to_remove, self)) 164 | 165 | def get_capacity(self): 166 | """ Provides the left capacity of the current connection pool. 167 | 168 | :returns: The left capacity 169 | """ 170 | return self._cache_size - len(self._connections) 171 | 172 | def filter_new_server_identifiers(self, server_identifiers, identifier_to_exclude=None): 173 | """ Provides all given identifiers which are not known until now. 174 | 175 | :param server_identifiers: Server identifiers to check against known ones 176 | :param identifier_to_exclude: (optional) Server identifiers to exclude 177 | :returns: Identifiers which are not known as server identifiers in the connection pool until now 178 | """ 179 | known_server_identifiers = self.get_server_identifiers(identifier_to_exclude=identifier_to_exclude) 180 | new_identifiers = [] 181 | for server_identifier in server_identifiers: 182 | if server_identifier not in known_server_identifiers: 183 | new_identifiers.append(server_identifier) 184 | return new_identifiers 185 | 186 | def get_random_identifier(self, identifier_to_exclude): 187 | """ Provides a random identifier which represents an active connection in the pool at the moment. 188 | 189 | :param identifier_to_exclude: Identifier to exclude 190 | :returns: Random identifier 191 | """ 192 | self._pool_lock.acquire() 193 | identifiers = [identifier for identifier in self._connections.keys() if identifier != identifier_to_exclude] 194 | self._pool_lock.release() 195 | if len(identifiers) > 1: 196 | return identifiers[random.randint(0, len(identifiers) - 1)] 197 | elif len(identifiers) == 1: 198 | return identifiers[0] 199 | else: 200 | return None 201 | -------------------------------------------------------------------------------- /gossip/communication/server.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import logging 16 | import multiprocessing 17 | import socket 18 | 19 | from gossip.communication.client_receiver import GossipClientReceiver 20 | 21 | __author__ = 'Anselm Binninger, Thomas Maier, Ralph Schaumann' 22 | 23 | 24 | class GossipServer(multiprocessing.Process): 25 | def __init__(self, server_label, client_receiver_label, bind_address, tcp_port, to_controller_queue, 26 | connection_pool): 27 | """ The Gossip server waits for new connections established by other clients. It also instantiates new receivers 28 | for incoming connections. 29 | 30 | :param server_label: A label to derive the concrete functionality of this gossip server 31 | :param client_receiver_label: This label is used for newly instantiated receivers 32 | :param bind_address: IPv4 address which is used to listen for new connections 33 | :param tcp_port: TCP port which is used to listen for new connections 34 | :param to_controller_queue: Newly instantiated receivers need to know a queue to communicate with the controller 35 | :param connection_pool: New connections will be added to the appropriate connection pool 36 | """ 37 | multiprocessing.Process.__init__(self) 38 | self.server_label = server_label 39 | self.client_receiver_label = client_receiver_label 40 | self.bind_address = bind_address 41 | self.tcp_port = tcp_port 42 | self.to_controller_queue = to_controller_queue 43 | self.connection_pool = connection_pool 44 | 45 | def run(self): 46 | """ Typical run method for the sender process. It waits for new connections, refers to newly instantiated 47 | receiver instances, and finally starts the new receivers. """ 48 | try: 49 | logging.info('%s started (%s:%d) - PID: %s' % (self.server_label, self.bind_address, self.tcp_port, 50 | self.pid)) 51 | server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 52 | server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 53 | server_socket.bind((self.bind_address, self.tcp_port)) 54 | server_socket.listen(5) 55 | while True: 56 | client_socket, address = server_socket.accept() 57 | tcp_address, tcp_port = address 58 | connection_identifier = '%s:%d' % (tcp_address, tcp_port) 59 | self.connection_pool.add_connection(connection_identifier, client_socket) 60 | logging.info("%s | Added new connection to connection pool" % self.server_label) 61 | client_receiver = GossipClientReceiver(self.client_receiver_label, client_socket, tcp_address, tcp_port, 62 | self.to_controller_queue, self.connection_pool) 63 | client_receiver.start() 64 | server_socket.close() 65 | except OSError as os_error: 66 | logging.error('%s crashed (%s:%d) - PID: %s - %s' % (self.server_label, self.bind_address, self.tcp_port, 67 | self.pid, os_error)) 68 | -------------------------------------------------------------------------------- /gossip/control/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | Submodule which handles the incoming API and P2P messages/connections/sockets. 17 | 18 | It is called from the API and P2P layer asynchronously. 19 | """ 20 | 21 | __author__ = 'Anselm Binninger, Thomas Maier, Ralph Schaumann' 22 | -------------------------------------------------------------------------------- /gossip/control/api_controller.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import multiprocessing 16 | import logging 17 | 18 | from gossip.control import convert 19 | from gossip.util.message_code import MESSAGE_CODE_ANNOUNCE, MESSAGE_CODE_NOTIFY, MESSAGE_CODE_VALIDATION 20 | from gossip.util.queue_item_types import QUEUE_ITEM_TYPE_SEND_MESSAGE, QUEUE_ITEM_TYPE_CONNECTION_LOST, \ 21 | QUEUE_ITEM_TYPE_RECEIVED_MESSAGE 22 | 23 | __author__ = 'Anselm Binninger, Thomas Maier, Ralph Schaumann' 24 | 25 | 26 | class APIController(multiprocessing.Process): 27 | def __init__(self, from_api_queue, to_api_queue, to_p2p_queue, api_connection_pool, p2p_connection_pool, 28 | announce_message_cache, api_registration_handler): 29 | """ This controller is responsible for all incoming messages from the API layer. If an API client sends any 30 | message, this controller handles it in various ways. 31 | 32 | :param from_api_queue: Used by the API layer for incoming messages and commands 33 | :param to_api_queue: Messages and commands for the API layer are sent through this queue 34 | :param to_p2p_queue: Messages and commands for the P2P layer are sent through this queue 35 | :param api_connection_pool: Pool which contains all API connections/clients/sockets 36 | :param p2p_connection_pool: Pool which contains all P2P connections/clients/sockets 37 | :param announce_message_cache: Message cache which contains announce messages. 38 | :param api_registration_handler: Used for registrations (via NOTIFY message) from API clients 39 | """ 40 | multiprocessing.Process.__init__(self) 41 | self.from_api_queue = from_api_queue 42 | self.to_api_queue = to_api_queue 43 | self.to_p2p_queue = to_p2p_queue 44 | self.api_connection_pool = api_connection_pool 45 | self.p2p_connection_pool = p2p_connection_pool 46 | self.announce_message_cache = announce_message_cache 47 | self.api_registration_handler = api_registration_handler 48 | 49 | def run(self): 50 | """ Typical run method which is used to handle API messages and commands. It reacts on incoming messages with 51 | changing the state of Gossip internally or by sending new messages resp. establishing new connections. """ 52 | logging.info('%s started - PID: %s' % (type(self).__name__, self.pid)) 53 | while True: 54 | queue_item = self.from_api_queue.get() 55 | queue_item_type = queue_item['type'] 56 | message = queue_item['message'] 57 | senders_identifier = queue_item['identifier'] 58 | 59 | if queue_item_type == QUEUE_ITEM_TYPE_RECEIVED_MESSAGE: 60 | msg_code = message.get_values()['code'] 61 | 62 | if msg_code == MESSAGE_CODE_ANNOUNCE: 63 | logging.debug('APIController | Handle received announce (%d): %s' % (MESSAGE_CODE_ANNOUNCE, 64 | message)) 65 | 66 | # Spread message via API layer (only registered clients) if it's unknown until now 67 | msg_id = self.announce_message_cache.add_message(message, valid=True) 68 | 69 | if msg_id: 70 | logging.info('APIController | Spread message (id: %d) through API layer' % msg_id) 71 | 72 | # Communication with API clients works with notification messages only. Therefore we have to 73 | # convert the announce message. 74 | notification_msg = convert.from_announce_to_notification(msg_id, message) 75 | for receiver in self.api_registration_handler.get_registrations(message.data_type): 76 | if receiver != senders_identifier: 77 | self.to_api_queue.put({'type': QUEUE_ITEM_TYPE_SEND_MESSAGE, 'identifier': receiver, 78 | 'message': notification_msg}) 79 | 80 | # Spread message via P2P layer (all peers!) 81 | logging.info('APIController | Spread message (id: %d) through P2P layer' % msg_id) 82 | for receiver in self.p2p_connection_pool.get_identifiers(): 83 | self.to_p2p_queue.put({'type': QUEUE_ITEM_TYPE_SEND_MESSAGE, 'identifier': receiver, 84 | 'message': message}) 85 | else: 86 | logging.info('APIController | Discard message (already known).') 87 | 88 | elif msg_code == MESSAGE_CODE_NOTIFY: 89 | logging.debug('APIController | Handle received notify (%d): %s' % (MESSAGE_CODE_NOTIFY, message)) 90 | msg_type = message.data_type 91 | self.api_registration_handler.register(msg_type, senders_identifier) 92 | logging.debug('APIController | API client is registered for message code %d now' % msg_type) 93 | # Api client has registered, send him all messages that are in cache currently 94 | for known_message in self.announce_message_cache.iterator(exclude_id=False): 95 | notification_msg = convert.from_announce_to_notification(known_message[0], 96 | known_message[1]["message"]) 97 | self.spread_message_to_api(notification_msg, senders_identifier) 98 | # TODO: Delete the registration again if the connection has been terminated! 99 | 100 | elif msg_code == MESSAGE_CODE_VALIDATION: 101 | logging.debug('APIController | Handle received validation (%d): %s' % (MESSAGE_CODE_VALIDATION, 102 | message)) 103 | 104 | msg_id = message.get_values()['id'] 105 | # React on validation only if we haven't done that already 106 | if not self.announce_message_cache.is_valid(msg_id): 107 | if message.get_values()['valid']: 108 | # Mark message as valid 109 | self.announce_message_cache.set_validity(msg_id, True) 110 | logging.debug('APIController | Message is valid (id: %d)' % msg_id) 111 | 112 | # Spread message if it's still present in the cache 113 | message_to_spread = self.announce_message_cache.get_message(msg_id) 114 | if message_to_spread: 115 | # Spread message over P2P layer 116 | logging.info('APIController | Spread message (id: %d) through P2P layer' % msg_id) 117 | # TODO: Don't send the message to the original sender! Exclude his identifier! 118 | for identifier in self.p2p_connection_pool.get_identifiers(): 119 | self.to_p2p_queue.put({'type': QUEUE_ITEM_TYPE_SEND_MESSAGE, 120 | 'identifier': identifier, 'message': message_to_spread}) 121 | else: 122 | logging.debug('APIController | Message (id: %d) not in cache anymore.' 123 | ' Spreading impossible' % msg_id) 124 | else: 125 | logging.debug('APIController | Message is invalid (id: %d)' % msg_id) 126 | self.announce_message_cache.remove_message(msg_id) 127 | else: 128 | logging.debug('APIController | Already spreaded the message (id: %d)' % msg_id) 129 | 130 | else: 131 | logging.debug('APIController | Discarding message: %s' % message) 132 | elif queue_item_type == QUEUE_ITEM_TYPE_CONNECTION_LOST: 133 | self.api_registration_handler.unregister(senders_identifier) 134 | logging.debug('APIController | Lost an API connection: %s' % senders_identifier) 135 | 136 | @staticmethod 137 | def fetch_identifiers(connection_pool, server_to_exclude): 138 | """ Collects server identifiers 139 | :param connection_pool: The connection pool to iterate through 140 | :param server_to_exclude: Server identifier to exclude 141 | """ 142 | for identifier in connection_pool.get_identifiers(): 143 | server_address = connection_pool.get_connection(identifier)['server_address'] 144 | if server_address != server_to_exclude: 145 | yield server_address 146 | 147 | def spread_message_to_api(self, notification_msg, senders_identifier): 148 | """ Spreads a message to all API clients which are registered for the containing message type. 149 | 150 | :param notification_msg: This message will be spread through all desired API clients 151 | :param senders_identifier: This identifier will be excluded from the receivers list 152 | """ 153 | logging.debug("APIControllor | Spreading known messages to api") 154 | for receiver in self.api_registration_handler.get_registrations(notification_msg.data_type): 155 | # Only send messages to newly registered api! 156 | if receiver == senders_identifier: 157 | self.to_api_queue.put({'type': QUEUE_ITEM_TYPE_SEND_MESSAGE, 'identifier': receiver, 158 | 'message': notification_msg}) 159 | -------------------------------------------------------------------------------- /gossip/control/api_registrations.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from multiprocessing import Manager 16 | 17 | __author__ = 'Anselm Binninger, Thomas Maier, Ralph Schaumann' 18 | 19 | 20 | class APIRegistrationHandler: 21 | """Thread-safe implementation of an handler for API registrations.""" 22 | 23 | def __init__(self): 24 | """ Contructor. """ 25 | self._api_registrations = Manager().dict() 26 | 27 | def register(self, code, identifier): 28 | """ Registers an identifier for a specified code. 29 | 30 | :param code: The code a particular identifier wants to register for 31 | :param identifier: The identifier who wants to register for the code 32 | """ 33 | if code not in self._api_registrations.keys(): 34 | self._api_registrations[code] = [] 35 | 36 | if identifier not in self._api_registrations[code]: 37 | self._api_registrations[code] += [identifier] 38 | 39 | def unregister(self, identifier): 40 | """Removes a api from registrations 41 | 42 | :param identifier which should be removed from the registrations""" 43 | for code, registrations in self._api_registrations.items(): 44 | if identifier in registrations: 45 | registrations.remove(identifier) 46 | self._api_registrations[code] = registrations 47 | 48 | def get_registrations(self, code): 49 | """ Provides all identifiers who registered for a specified code. 50 | 51 | :param code: The code for which the registrations are returned 52 | :returns: All identifiers who registered for this code 53 | """ 54 | return self._api_registrations[code] if code in self._api_registrations else [] 55 | -------------------------------------------------------------------------------- /gossip/control/convert.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from gossip.util.packing import pack_gossip_notification 16 | from gossip.util.message import MessageGossipNotification 17 | 18 | __author__ = 'Anselm Binninger, Thomas Maier, Ralph Schaumann' 19 | 20 | 21 | def from_announce_to_notification(msg_id, message): 22 | """ 23 | Method by which the values of an announce message are transferred into a notification message 24 | 25 | :param msg_id: the message id of the new notification message 26 | :param message: the message to convert 27 | :return: the packed converted message object 28 | """ 29 | message_values = message.get_values() 30 | msg_data = pack_gossip_notification(msg_id, message_values['type'], message_values['message'])['data'] 31 | return MessageGossipNotification(msg_data) 32 | -------------------------------------------------------------------------------- /gossip/control/message_cache.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import logging 16 | from multiprocessing import Manager 17 | from random import randrange 18 | 19 | from datetime import datetime 20 | 21 | __author__ = 'Anselm Binninger, Thomas Maier, Ralph Schaumann' 22 | 23 | 24 | class GossipMessageCache: 25 | """Thread-safe implementation of a message cache which maintains all currently known messages.""" 26 | 27 | DATE_ADDED = 'DateAdded' 28 | MAX_MSG_ID = 65535 29 | 30 | def __init__(self, message_cache_label, cache_size=30): 31 | """Contructor. 32 | 33 | :param cache_size: The maximum numbers of messages that can be hold by this cache: Default 30 34 | """ 35 | self._msg_cache = Manager().dict() 36 | self._message_cache_label = message_cache_label 37 | self._cache_size = cache_size 38 | 39 | def add_message(self, message, valid=False): 40 | """ Adds new message to the cache. 41 | 42 | :param message: The new message to cache 43 | :param valid: (optional) Flag which states whether this message is valid or not 44 | :returns: The generated random message identifier for the cached message (None if message is already in cache) 45 | """ 46 | # If the message exists already in the cache, return None 47 | for msg_id in self._msg_cache.keys(): 48 | if message == self._msg_cache[msg_id]['message']: 49 | return None 50 | 51 | # Generate a message id which isn't in the cache already 52 | msg_id = randrange(0, self.MAX_MSG_ID) 53 | while msg_id in self._msg_cache.keys(): 54 | msg_id = randrange(0, self.MAX_MSG_ID) 55 | self._msg_cache[msg_id] = {'message': message, 'valid': valid, 56 | GossipMessageCache.DATE_ADDED: datetime.now()} 57 | 58 | self.__maintain_cache() 59 | logging.debug('%s | Added new message, current message cache: %s' % (self._message_cache_label, 60 | self._msg_cache.keys())) 61 | return msg_id 62 | 63 | def get_message(self, msg_id): 64 | """ Provides a cached message. 65 | 66 | :param msg_id: Identifier of the desired message 67 | :returns: The desired message (None if it does not exist) 68 | """ 69 | logging.debug('%s | Current message cache: %s' % (self._message_cache_label, self._msg_cache.keys())) 70 | if msg_id in self._msg_cache: 71 | return self._msg_cache[msg_id]['message'] 72 | else: 73 | return None 74 | 75 | def is_valid(self, msg_id): 76 | """ Returns True if the specified message is marked as valid 77 | 78 | :param msg_id: Message id 79 | :returns: True if the message is marked as valid, False if it is invalid or if it doesn't exist in the cache 80 | """ 81 | if msg_id in self._msg_cache: 82 | return self._msg_cache[msg_id]['valid'] 83 | else: 84 | return False 85 | 86 | def set_validity(self, msg_id, valid): 87 | """ Sets validity for a specific message. 88 | 89 | :param msg_id: Identifier of the message 90 | :param valid: Flag which states whether the specified message is valid or not 91 | """ 92 | if msg_id in self._msg_cache: 93 | old_cache_item = self._msg_cache[msg_id] 94 | old_cache_item['valid'] = valid 95 | self._msg_cache[msg_id] = old_cache_item 96 | 97 | def remove_message(self, msg_id): 98 | """ Removes a message from the cache. 99 | 100 | :param msg_id: Identifier of the message 101 | :returns: Removed message (None if it does not exist) 102 | """ 103 | return self._msg_cache.pop(msg_id, None) 104 | 105 | def __maintain_cache(self): 106 | """ Maintains the message cache. If the cache exceeds the defined maximum the oldest message is removed from the 107 | cache """ 108 | if len(self._msg_cache) > self._cache_size: 109 | sorted_messages = sorted(self._msg_cache.items(), 110 | key=lambda x: self._msg_cache[x[0]][GossipMessageCache.DATE_ADDED]) 111 | message_to_remove = sorted_messages[0][0] 112 | self.remove_message(message_to_remove) 113 | 114 | def iterator(self, exclude_id=True): 115 | """ Creates a generator for the message cache. Removes the outer dict with id and only returns message ordered 116 | by date (from oldest to newest) 117 | 118 | :return: An iterator over the ordered list of messages 119 | """ 120 | sorted_messages = sorted(self._msg_cache.items(), 121 | key=lambda x: self._msg_cache[x[0]][GossipMessageCache.DATE_ADDED]) 122 | return (message[1] if exclude_id else message for message in sorted_messages) 123 | -------------------------------------------------------------------------------- /gossip/control/p2p_controller.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import multiprocessing 16 | import logging 17 | 18 | from gossip.control import convert 19 | from gossip.util.message import MessageGossipPeerResponse, MessageGossipPeerRequest, MessageGossipPeerInit, \ 20 | MessageGossipPeerUpdate, MessageGossipAnnounce 21 | from gossip.util.packing import pack_gossip_peer_response, pack_gossip_peer_request, pack_gossip_peer_init, \ 22 | pack_gossip_peer_update, pack_gossip_announce, PEER_UPDATE_TYPE_PEER_LOST, PEER_UPDATE_TYPE_PEER_FOUND 23 | from gossip.util.message_code import MESSAGE_CODE_ANNOUNCE, MESSAGE_CODE_PEER_REQUEST, MESSAGE_CODE_PEER_RESPONSE, \ 24 | MESSAGE_CODE_PEER_UPDATE, MESSAGE_CODE_PEER_INIT 25 | from gossip.util.queue_item_types import QUEUE_ITEM_TYPE_SEND_MESSAGE, QUEUE_ITEM_TYPE_CONNECTION_LOST, \ 26 | QUEUE_ITEM_TYPE_RECEIVED_MESSAGE, QUEUE_ITEM_TYPE_ESTABLISH_CONNECTION, QUEUE_ITEM_TYPE_NEW_CONNECTION 27 | 28 | __author__ = 'Anselm Binninger, Thomas Maier, Ralph Schaumann' 29 | 30 | 31 | class P2PController(multiprocessing.Process): 32 | def __init__(self, from_p2p_queue, to_p2p_queue, to_api_queue, p2p_connection_pool, p2p_server_address, 33 | announce_message_cache, update_message_cache, api_registration_handler, max_ttl, 34 | bootstrapper_address=None): 35 | """ This controller is responsible for all incoming messages from the P2P layer. If a P2P client sends any 36 | message, this controller handles it in various ways. 37 | 38 | :param from_p2p_queue: Used by the P2P layer for incoming messages and commands 39 | :param to_p2p_queue: Messages and commands for the P2P layer are sent through this queue 40 | :param to_api_queue: Messages and commands for the API layer are sent through this queue 41 | :param p2p_connection_pool: Pool which contains all P2P connections/clients/sockets 42 | :param p2p_server_address: The P2P server address for this gossip instance 43 | :param announce_message_cache: Message cache which contains announce messages. 44 | :param update_message_cache: Message cache for peer update messages 45 | :param api_registration_handler: Used for registrations (via NOTIFY message) from API clients 46 | :param max_ttl: Max. amount of hops until messages will be dropped 47 | :param bootstrapper_address: (optional) dict to specify the bootstrapper {'host': : 'port': } 48 | """ 49 | multiprocessing.Process.__init__(self) 50 | self.from_p2p_queue = from_p2p_queue 51 | self.to_p2p_queue = to_p2p_queue 52 | self.to_api_queue = to_api_queue 53 | self.p2p_connection_pool = p2p_connection_pool 54 | self.p2p_server_address = p2p_server_address 55 | self.announce_message_cache = announce_message_cache 56 | self.update_message_cache = update_message_cache 57 | self.api_registration_handler = api_registration_handler 58 | self.max_ttl = max_ttl 59 | self.bootstrapper_address = bootstrapper_address 60 | 61 | def run(self): 62 | """ Typical run method which is used to handle P2P messages and commands. It reacts on incoming messages with 63 | changing the state of Gossip internally or by sending new messages resp. establishing new connections. """ 64 | logging.info('%s started - PID: %s' % (type(self).__name__, self.pid)) 65 | 66 | # Bootstrapping part 67 | if self.bootstrapper_address: 68 | bootstrapper_identifier = '%s:%d' % (self.bootstrapper_address['host'], self.bootstrapper_address['port']) 69 | self.to_p2p_queue.put({'type': QUEUE_ITEM_TYPE_ESTABLISH_CONNECTION, 'identifier': bootstrapper_identifier}) 70 | self.send_peer_request(bootstrapper_identifier) 71 | 72 | # Usual controller part 73 | while True: 74 | queue_item = self.from_p2p_queue.get() 75 | queue_item_type = queue_item['type'] 76 | message = queue_item['message'] 77 | senders_identifier = queue_item['identifier'] 78 | 79 | if queue_item_type == QUEUE_ITEM_TYPE_RECEIVED_MESSAGE: 80 | msg_code = message.get_values()['code'] 81 | 82 | if msg_code == MESSAGE_CODE_ANNOUNCE: 83 | logging.debug('P2PController | Handle received announce (%d): %s' % (MESSAGE_CODE_ANNOUNCE, 84 | message)) 85 | 86 | # Spread message via API layer (only registered clients) if it's unknown until now 87 | msg_id = self.announce_message_cache.add_message(message) 88 | 89 | if msg_id: 90 | logging.info('P2PController | Spread message (id: %d) through API layer' % msg_id) 91 | 92 | # Change ttl and create new announce message 93 | ttl = message.get_values()['TTL'] 94 | if ttl > 1 or ttl == 0: 95 | ttl = ttl-1 if ttl > 1 else 0 96 | packed_announce_msg = pack_gossip_announce(ttl, message.get_values()['type'], 97 | message.get_values()['message'])['data'] 98 | announce_msg = MessageGossipAnnounce(packed_announce_msg) 99 | 100 | # Communication with API clients works with notification messages only. Therefore we have to 101 | # convert the announce message. 102 | notification_msg = convert.from_announce_to_notification(msg_id, announce_msg) 103 | for receiver in self.api_registration_handler.get_registrations(message.data_type): 104 | if receiver != senders_identifier: 105 | self.to_api_queue.put({'type': QUEUE_ITEM_TYPE_SEND_MESSAGE, 'identifier': receiver, 106 | 'message': notification_msg}) 107 | else: 108 | logging.info('P2PController | Discard message (already known).') 109 | 110 | elif msg_code == MESSAGE_CODE_PEER_REQUEST: 111 | # Someone wants to know our known identifiers 112 | logging.debug('P2PController | Handle received peer request (%d): %s' % (MESSAGE_CODE_PEER_REQUEST, 113 | message)) 114 | 115 | # The peer request message contains the server address of the other peer 116 | peer_server_identifier = message.get_values()['p2p_server_address'] 117 | self.p2p_connection_pool.update_connection(senders_identifier, peer_server_identifier) 118 | 119 | # Build identifier list BUT exclude the identifier of the requesting peer! 120 | own_p2p_server_identifier = '%s:%d' % (self.p2p_server_address['host'], 121 | self.p2p_server_address['port']) 122 | known_server_identifiers = self.p2p_connection_pool.get_server_identifiers( 123 | identifier_to_exclude=[peer_server_identifier, own_p2p_server_identifier]) 124 | 125 | # Send the assembled identifier list 126 | packed_data = pack_gossip_peer_response(known_server_identifiers)['data'] 127 | peer_response_msg = MessageGossipPeerResponse(packed_data) 128 | self.to_p2p_queue.put({'type': QUEUE_ITEM_TYPE_SEND_MESSAGE, 'identifier': senders_identifier, 129 | 'message': peer_response_msg}) 130 | logging.debug('P2PController | Answering with peer response (%d): %s' % (MESSAGE_CODE_PEER_RESPONSE, 131 | peer_response_msg)) 132 | 133 | # We've got the server identifier with the peer request, so spread it to anyone we know 134 | senders_server_identifier = self.p2p_connection_pool.get_server_identifier(senders_identifier) 135 | self.send_peer_update(senders_identifier, senders_server_identifier, self.max_ttl) 136 | 137 | elif msg_code == MESSAGE_CODE_PEER_INIT: 138 | # Someone wants to inform us about his server identifier 139 | logging.debug('P2PController | Handle received peer init (%d): %s' % (MESSAGE_CODE_PEER_INIT, 140 | message)) 141 | 142 | # The peer request message contains the server address of the other peer 143 | peer_server_identifier = message.get_values()['p2p_server_address'] 144 | self.p2p_connection_pool.update_connection(senders_identifier, peer_server_identifier) 145 | 146 | # We've got the server identifier with the peer init, so spread it to anyone we know 147 | senders_server_identifier = self.p2p_connection_pool.get_server_identifier(senders_identifier) 148 | self.send_peer_update(senders_identifier, senders_server_identifier, self.max_ttl) 149 | 150 | elif msg_code == MESSAGE_CODE_PEER_RESPONSE: 151 | # We received the known identifiers of someone 152 | logging.debug('P2PController | Handle received peer response (%d): %s' 153 | % (MESSAGE_CODE_PEER_RESPONSE, message)) 154 | 155 | # Use the peer response only if there is space for new connections in the pool 156 | if self.p2p_connection_pool.get_capacity() > 0: 157 | received_server_identifiers = message.get_values()['data'] 158 | new_identifiers = self.p2p_connection_pool.filter_new_server_identifiers( 159 | received_server_identifiers) 160 | 161 | # If the peer response provides new identifiers, we establish a new connection with them 162 | if len(new_identifiers) > 0: 163 | for new_identifier in new_identifiers: 164 | self.to_p2p_queue.put({'type': QUEUE_ITEM_TYPE_ESTABLISH_CONNECTION, 165 | 'identifier': new_identifier}) 166 | 167 | # Send initial message 168 | logging.debug('P2PController | Sending peer init (%d): %s' % (MESSAGE_CODE_PEER_INIT, 169 | message)) 170 | own_p2p_server_identifier = '%s:%d' % (self.p2p_server_address['host'], 171 | self.p2p_server_address['port']) 172 | packed_data = pack_gossip_peer_init(own_p2p_server_identifier)['data'] 173 | self.to_p2p_queue.put({'type': QUEUE_ITEM_TYPE_SEND_MESSAGE, 174 | 'identifier': new_identifier, 175 | 'message': MessageGossipPeerInit(packed_data)}) 176 | 177 | # Stop if the pool is full 178 | if self.p2p_connection_pool.get_capacity() <= 0: 179 | break 180 | else: 181 | logging.debug('P2PController | Discarding message (%d) because pool is full!' % msg_code) 182 | 183 | elif msg_code == MESSAGE_CODE_PEER_UPDATE: 184 | # We received a peer update of someone 185 | logging.debug('P2PController | Handle received peer update (%d): %s' % (MESSAGE_CODE_PEER_UPDATE, 186 | message)) 187 | 188 | new_server_identifier = message.get_values()['address'] 189 | update_type = message.get_values()['update_type'] 190 | ttl = message.get_values()['ttl'] 191 | 192 | if ttl < int(self.max_ttl/2): 193 | if update_type == PEER_UPDATE_TYPE_PEER_FOUND: 194 | # Use the peer update only if there is space for a new connection in the pool 195 | if self.p2p_connection_pool.get_capacity() > 0: 196 | new_identifiers = self.p2p_connection_pool.filter_new_server_identifiers( 197 | [new_server_identifier]) 198 | 199 | # If the peer update provides a new identifier, we establish a new connection with it 200 | for new_identifier in new_identifiers: 201 | self.to_p2p_queue.put({'type': QUEUE_ITEM_TYPE_ESTABLISH_CONNECTION, 202 | 'identifier': new_identifier}) 203 | 204 | # Send initial message 205 | logging.debug('P2PController | Sending peer init (%d): %s' % (MESSAGE_CODE_PEER_INIT, 206 | message)) 207 | own_p2p_server_identifier = '%s:%d' % (self.p2p_server_address['host'], 208 | self.p2p_server_address['port']) 209 | packed_data = pack_gossip_peer_init(own_p2p_server_identifier)['data'] 210 | self.to_p2p_queue.put({'type': QUEUE_ITEM_TYPE_SEND_MESSAGE, 211 | 'identifier': new_identifier, 212 | 'message': MessageGossipPeerInit(packed_data)}) 213 | else: 214 | logging.debug('P2PController | Discarding message (%d) because pool is full' % msg_code) 215 | elif update_type == PEER_UPDATE_TYPE_PEER_LOST: 216 | # Currently a peer update of type PEER_UPDATE_TYPE_PEER_LOST does not need to be handled 217 | pass 218 | 219 | # If we don't know the peer update already, spread it 220 | if ttl > 1: 221 | ttl -= 1 222 | self.send_peer_update(senders_identifier, new_server_identifier, ttl) 223 | elif ttl == 0: # A ttl of 0 means that the message is unstoppable! 224 | self.send_peer_update(senders_identifier, new_server_identifier, ttl) 225 | 226 | else: 227 | logging.debug('P2PController | Discarding message (%d)' % msg_code) 228 | 229 | elif queue_item_type == QUEUE_ITEM_TYPE_CONNECTION_LOST: 230 | # A connection has been disconnected from this instance 231 | logging.debug('P2PController | One connection lost, try to get a new one %s' % senders_identifier) 232 | 233 | random_identifier = self.p2p_connection_pool.get_random_identifier(senders_identifier) 234 | if random_identifier: 235 | self.send_peer_request(random_identifier) 236 | 237 | elif queue_item_type == QUEUE_ITEM_TYPE_NEW_CONNECTION: 238 | # Our instance know a new connection 239 | senders_server_identifier = self.p2p_connection_pool.get_server_identifier(senders_identifier) 240 | # We can inform everyone only if we know the server identifier of the sender 241 | if senders_server_identifier: 242 | self.send_peer_update(senders_identifier, senders_server_identifier, self.max_ttl) 243 | else: 244 | logging.debug('P2PController | Don\'t know the server identifier of the new connection, wait for' 245 | ' peer server address of %s' % senders_identifier) 246 | 247 | self.exchange_messages(senders_identifier) 248 | 249 | def send_peer_update(self, senders_identifier, senders_server_identifier, ttl): 250 | """ Sends peer updates to several peers. 251 | 252 | :param senders_identifier: Identifier of the sender we received this update from 253 | :param senders_server_identifier: Server identifier of the changed peer 254 | :param ttl: ttl to set in the new update messages 255 | """ 256 | packed_data = pack_gossip_peer_update(senders_server_identifier, ttl, PEER_UPDATE_TYPE_PEER_FOUND)['data'] 257 | peer_update_msg = MessageGossipPeerUpdate(packed_data) 258 | msg_id = self.update_message_cache.add_message(peer_update_msg, valid=True) 259 | 260 | if msg_id and senders_server_identifier != '%s:%d' % (self.p2p_server_address['host'], 261 | self.p2p_server_address['port']): 262 | logging.debug('P2PController | Spread information about new connection %s' % senders_identifier) 263 | identifiers = self.p2p_connection_pool.get_identifiers() 264 | for identifier in identifiers: 265 | if identifier not in [senders_identifier, senders_server_identifier]: 266 | self.to_p2p_queue.put({'type': QUEUE_ITEM_TYPE_SEND_MESSAGE, 'identifier': identifier, 267 | 'message': peer_update_msg}) 268 | 269 | def send_peer_request(self, peer_request_identifier): 270 | """ Sends a peer request 271 | 272 | :param peer_request_identifier: The identifier dict of the receiving peer 273 | """ 274 | own_p2p_server_identifier = '%s:%d' % (self.p2p_server_address['host'], self.p2p_server_address['port']) 275 | packed_msg = pack_gossip_peer_request(own_p2p_server_identifier) 276 | peer_request_msg = MessageGossipPeerRequest(packed_msg['data']) 277 | self.to_p2p_queue.put({'type': QUEUE_ITEM_TYPE_SEND_MESSAGE, 'identifier': peer_request_identifier, 278 | 'message': peer_request_msg}) 279 | 280 | def exchange_messages(self, peer_identifier): 281 | """ Send messages to new connected peer. 282 | 283 | :param peer_identifier: Receiving peer 284 | """ 285 | logging.debug('P2PController | Exchanging messages with (%s)' % peer_identifier) 286 | for message in self.announce_message_cache.iterator(): 287 | self.to_p2p_queue.put({'type': QUEUE_ITEM_TYPE_SEND_MESSAGE, 'identifier': peer_identifier, 288 | 'message': message["message"]}) 289 | -------------------------------------------------------------------------------- /gossip/gossip_main.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import logging 16 | import logging.config 17 | import os 18 | import signal 19 | import sys 20 | from argparse import ArgumentParser 21 | from multiprocessing import Queue 22 | 23 | from gossip.communication.server import GossipServer 24 | from gossip.communication.client_sender import GossipSender 25 | from gossip.communication.connection import GossipConnectionPool 26 | from gossip.control.api_controller import APIController 27 | from gossip.control.p2p_controller import P2PController 28 | from gossip.control.message_cache import GossipMessageCache 29 | from gossip.control.api_registrations import APIRegistrationHandler 30 | from gossip.util import config_parser 31 | 32 | __author__ = 'Anselm Binninger, Thomas Maier, Ralph Schaumann' 33 | 34 | DEFAULT_CONFIG_PATH = 'config/config.ini' 35 | 36 | logging.config.fileConfig('config/logging_config.ini') 37 | 38 | 39 | def signal_handler(signal, frame): 40 | logging.error('Stopping gossip process - PID: %s', (os.getpid())) 41 | sys.exit(0) 42 | 43 | 44 | def main(): 45 | cli_parser = ArgumentParser() 46 | cli_parser.add_argument('-c', '--config', help='Configuration file path', default=DEFAULT_CONFIG_PATH) 47 | cli_args = cli_parser.parse_args() 48 | config_file_path = cli_args.config 49 | 50 | # Check cli arguments 51 | if not os.path.isfile(config_file_path): 52 | logging.error('The given config file does not exist: %s', config_file_path) 53 | sys.exit(1) 54 | 55 | """ Runs the gossip module. """ 56 | logging.info('Starting gossip main process - PID: %s', (os.getpid())) 57 | 58 | # Register SIGINT signal 59 | signal.signal(signal.SIGINT, signal_handler) 60 | 61 | # Read 62 | gossip_config = config_parser.read_config(config_file_path) 63 | 64 | # Gossip layers with queues between them: 65 | # 66 | # <---- API connections <---- 67 | # v ^ 68 | # APIServer APISender 69 | # v ^ 70 | # ---------------- ---------------- 71 | # | API2Controller | | Controller2API | 72 | # ---------------- ---------------- 73 | # v ^ 74 | # APIController P2PController 75 | # v ^ 76 | # ---------------- ---------------- 77 | # | Controller2P2P | | P2P2Controller | 78 | # ---------------- ---------------- 79 | # v ^ 80 | # P2PSender P2PServer 81 | # v ^ 82 | # ----> P2P connections ----> 83 | 84 | api_server_address = gossip_config['api_address'] 85 | p2p_server_address = gossip_config['listen_address'] 86 | bootstrapper_address = gossip_config['bootstrapper'] 87 | max_connections = gossip_config['max_connections'] 88 | cache_size = gossip_config['cache_size'] 89 | max_ttl = gossip_config['max_ttl'] 90 | 91 | api_connection_pool = GossipConnectionPool('APIConnectionPool', cache_size=max_connections) 92 | p2p_connection_pool = GossipConnectionPool('P2PConnectionPool', cache_size=max_connections) 93 | announce_message_cache = GossipMessageCache('AnnounceMessageCache', cache_size=cache_size) 94 | update_message_cache = GossipMessageCache('UpdateMessageCache', cache_size=cache_size) 95 | 96 | api_registration_handler = APIRegistrationHandler() 97 | 98 | # Layers for incoming API connections/messages 99 | api_to_controller = Queue() 100 | api_server = GossipServer('APIServer', 'APIClientReceiver', api_server_address['host'], api_server_address['port'], 101 | api_to_controller, api_connection_pool) 102 | controller_to_p2p = Queue() 103 | controller_to_api = Queue() 104 | api_controller = APIController(api_to_controller, controller_to_api, controller_to_p2p, api_connection_pool, 105 | p2p_connection_pool, announce_message_cache, api_registration_handler) 106 | p2p_to_controller = Queue() 107 | p2p_sender = GossipSender('P2PSender', controller_to_p2p, p2p_to_controller, p2p_connection_pool) 108 | 109 | # Layers for incoming P2P connections/messages 110 | p2p_server = GossipServer('P2PServer', 'P2PClientReceiver', p2p_server_address['host'], p2p_server_address['port'], 111 | p2p_to_controller, p2p_connection_pool) 112 | p2p_controller = P2PController(p2p_to_controller, controller_to_p2p, controller_to_api, p2p_connection_pool, 113 | p2p_server_address, announce_message_cache, update_message_cache, 114 | api_registration_handler, max_ttl, bootstrapper_address=bootstrapper_address) 115 | api_sender = GossipSender('APISender', controller_to_api, api_to_controller, api_connection_pool) 116 | 117 | api_server.start() 118 | api_controller.start() 119 | p2p_sender.start() 120 | p2p_server.start() 121 | p2p_controller.start() 122 | api_sender.start() 123 | 124 | api_server.join() 125 | api_controller.join() 126 | p2p_sender.join() 127 | p2p_server.join() 128 | p2p_controller.join() 129 | api_sender.join() 130 | 131 | # Handle exit codes 132 | exit_codes = api_server.exitcode | api_controller.exitcode | p2p_sender.exitcode 133 | exit_codes = exit_codes | p2p_server.exitcode | p2p_controller.exitcode | api_sender.exitcode 134 | 135 | if exit_codes > 0: 136 | logging.error('Gossip subprocess exited with return code %d', exit_codes) 137 | else: 138 | logging.info('Gossip exited with return code %d', exit_codes) 139 | 140 | sys.exit(exit_codes) 141 | -------------------------------------------------------------------------------- /gossip/util/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | Submodule which contains different utility functions for usage all over Gossip. 17 | """ 18 | 19 | __author__ = 'Anselm Binninger, Thomas Maier, Ralph Schaumann' 20 | -------------------------------------------------------------------------------- /gossip/util/byte_formatting.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | __author__ = 'Anselm Binninger, Thomas Maier, Ralph Schaumann' 16 | 17 | 18 | def short_to_bytes(short_value): 19 | """ 20 | Transforms a short to a byte array (using Big-Endian!) 21 | 22 | :param short_value: Any integer that is greater than 0 and smaller than 65535 23 | :return: A bytes array with the highest byte at position 0 and the lowest at position 1 24 | """ 25 | return bytes([int(short_value) >> 8]) + bytes([int(short_value) & 0xff]) 26 | 27 | 28 | def bytes_to_short(higher_byte, lower_byte): 29 | """ 30 | Transforms two bytes into a short (Using Big-Endian!) 31 | 32 | :param higher_byte: The byte at position 0 (Higher byte) 33 | :param lower_byte: The byte at position 1 (Lower byte) 34 | :return: The two bytes transformed into a short 35 | """ 36 | return (higher_byte << 8) | lower_byte 37 | -------------------------------------------------------------------------------- /gossip/util/config_parser.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from configparser import RawConfigParser 16 | import logging 17 | 18 | __author__ = 'Anselm Binninger, Thomas Maier, Ralph Schaumann' 19 | 20 | 21 | def split_host_address(host_address): 22 | """ Splits a host address into the host and the corresponding port (as int) in the form of a dictionary 23 | 24 | :returns: A dict in the form {'host': 'port': } 25 | """ 26 | if ':' not in host_address: 27 | return None 28 | host, port = host_address.split(':') 29 | return {'host': host, 'port': int(port)} 30 | 31 | 32 | def read_config(config_path): 33 | """ Reads the INI configuration file and returns all settings as a dict 34 | 35 | :param config_path: File path of the INI file. 36 | :returns: A dict in the form {: , ...} 37 | """ 38 | logging.debug('Parsing gossip configuration file.') 39 | config_parser = RawConfigParser() 40 | config_parser.read(config_path) 41 | 42 | # Parse INI file 43 | hostkey = config_parser.get('GLOBAL', 'HOSTKEY') 44 | cache_size = config_parser.getint('GOSSIP', 'cache_size') 45 | max_connections = config_parser.getint('GOSSIP', 'max_connections') 46 | bootstrapper = split_host_address(config_parser.get('GOSSIP', 'bootstrapper')) 47 | listen_address = split_host_address(config_parser.get('GOSSIP', 'listen_address')) 48 | api_address = split_host_address(config_parser.get('GOSSIP', 'api_address')) 49 | max_ttl = int(config_parser.get('GOSSIP', 'max_ttl')) 50 | 51 | # Build dictionary 52 | config = {'hostkey': hostkey, 'cache_size': cache_size, 'max_connections': max_connections, 53 | 'bootstrapper': bootstrapper, 'listen_address': listen_address, 'api_address': api_address, 54 | 'max_ttl': max_ttl} 55 | 56 | return config 57 | -------------------------------------------------------------------------------- /gossip/util/exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | __author__ = 'Anselm Binninger, Thomas Maier, Ralph Schaumann' 16 | 17 | 18 | class GossipMessageException(Exception): 19 | def __init__(self, msg): 20 | super().__init__(msg) 21 | 22 | 23 | class GossipClientDisconnectedException(Exception): 24 | def __init__(self, msg): 25 | super().__init__(msg) 26 | 27 | 28 | class GossipMessageFormatException(Exception): 29 | def __init__(self, msg): 30 | super().__init__(msg) 31 | 32 | 33 | class GossipQueueException(Exception): 34 | def __init__(self, msg): 35 | super().__init__(msg) 36 | 37 | 38 | class GossipIdentifierNotFound(Exception): 39 | def __init__(self, msg): 40 | super().__init__(msg) 41 | -------------------------------------------------------------------------------- /gossip/util/message.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import struct 16 | from abc import ABCMeta 17 | 18 | from gossip.util.message_code import MESSAGE_CODE_ANNOUNCE, MESSAGE_CODE_PEER_REQUEST, MESSAGE_CODE_PEER_RESPONSE, \ 19 | MESSAGE_CODE_NOTIFICATION, MESSAGE_CODE_NOTIFY, MESSAGE_CODE_PEER_UPDATE, MESSAGE_CODE_VALIDATION, \ 20 | MESSAGE_CODE_PEER_INIT 21 | 22 | from gossip.util.byte_formatting import short_to_bytes, bytes_to_short 23 | 24 | __author__ = 'Anselm Binninger, Thomas Maier, Ralph Schaumann' 25 | 26 | 27 | class MessageGossip: 28 | """ 29 | Baseclass for gossip messages 30 | """ 31 | 32 | __metaclass__ = ABCMeta 33 | 34 | def __init__(self, code, data): 35 | """ 36 | C'tor 37 | 38 | :param code: the code of this message 39 | """ 40 | self.code = code 41 | self.data = data 42 | 43 | def get_code(self): 44 | """ 45 | Method by which the code of this message is retrieved 46 | 47 | :return: the code of this message 48 | """ 49 | return self.code 50 | 51 | def get_values(self): 52 | """ 53 | Method by which the values of this message are retrieved 54 | 55 | :return: the values of this message 56 | """ 57 | pass 58 | 59 | def __str__(self): 60 | """ 61 | to string method 62 | 63 | :return: a string representation of this message 64 | """ 65 | return '%s' % self.get_values() 66 | 67 | def encode(self): 68 | """ 69 | Encodes this message into a byte array 70 | 71 | :return: a byte array with the encoded header and payload 72 | """ 73 | size = len(self.data) + 4 74 | # encode the size into two bytes 75 | b_size = short_to_bytes(size) 76 | # encode the message code into two bytes 77 | b_code = short_to_bytes(self.code) 78 | # add all the bytes up to construct the message 79 | b_msg = b_size + b_code + self.data 80 | return b_msg 81 | 82 | 83 | class MessageGossip51x(MessageGossip): 84 | """ 85 | Baseclass for gossip messages of code 510-519 86 | """ 87 | 88 | def __init__(self, code, data): 89 | """ 90 | C'Tor 91 | 92 | :param code: the code of this message 93 | """ 94 | super().__init__(code, data) 95 | 96 | 97 | class MessageOther: 98 | """ 99 | If a message can't be decoded properly a MessageOther will be returned. 100 | """ 101 | 102 | def __init__(self, message_code, message_data): 103 | """ 104 | C'Tor 105 | 106 | :param message_code: the code of the message 107 | :param message_data: the data of the message 108 | """ 109 | self.code = message_code 110 | self.data = message_data 111 | 112 | def get_values(self): 113 | """ 114 | Method by which the values of this message are retrieved 115 | 116 | :return: a dictionary with the values of this message (keys: code, message) 117 | """ 118 | return {'code': self.code, 'message': self.data} 119 | 120 | def encode(self): 121 | size = len(self.data) + 4 122 | # encode the size into two bytes 123 | b_size = short_to_bytes(size) 124 | b_code = short_to_bytes(self.code) 125 | # add all the bytes up to construct the message 126 | b_msg = b_size + b_code + self.data 127 | return b_msg 128 | 129 | def __str__(self): 130 | """ 131 | to string method 132 | 133 | :return: a string representation of this message 134 | """ 135 | return {'code': self.code, 'data': self.data}.__str__() 136 | 137 | 138 | class MessageGossipAnnounce(MessageGossip): 139 | """ 140 | GossipAnnounceMessage 141 | This messages are received from api connections. If you want to implement an api client for gossip 142 | its messages need to be structured like that 143 | """ 144 | 145 | def __init__(self, data): 146 | """ 147 | C'Tor 148 | 149 | :param bytes data: the data of the message 150 | """ 151 | super().__init__(MESSAGE_CODE_ANNOUNCE, data) 152 | self.ttl = struct.unpack('{}B'.format(1), data[:1])[0] 153 | data_type_fst, data_type_snd = struct.unpack('{}B'.format(2), data[2:4]) 154 | self.data_type = bytes_to_short(data_type_fst, data_type_snd) 155 | self.msg = struct.unpack('{}B'.format(len(data) - 4), data[4:len(data)]) 156 | 157 | def get_values(self): 158 | """ 159 | Method by which the values of this message are retrieved 160 | 161 | :return: a dictionary with the values of this message (keys: code, message, TTL, type) 162 | """ 163 | return {'message': self.msg, 'code': self.code, 'TTL': self.ttl, 'type': self.data_type} 164 | 165 | def __hash__(self): 166 | return hash((self.msg, self.code, self.data_type)) 167 | 168 | def __eq__(self, other): 169 | return (self.msg, self.code, self.data_type) == (other.msg, other.code, other.data_type) 170 | 171 | def __ne__(self, other): 172 | return not (self == other) 173 | 174 | 175 | class MessageGossipNotify(MessageGossip): 176 | """ 177 | This message is sent from the gossip instance to all apis that registered for this specific datatype 178 | An api client needs to implement this messageformat 179 | """ 180 | 181 | def __init__(self, data): 182 | """ 183 | C'Tor 184 | 185 | :param bytes data: the data of the message 186 | """ 187 | super().__init__(MESSAGE_CODE_NOTIFY, data) 188 | data_type_fst, data_type_snd = struct.unpack('{}B'.format(2), data[2:4]) 189 | self.data_type = bytes_to_short(data_type_fst, data_type_snd) 190 | 191 | def get_values(self): 192 | """ 193 | Method by which the values of this message are retrieved 194 | 195 | :return: a dictionary with the values of this message (keys: code, type) 196 | """ 197 | return {'type': self.data_type, 'code': self.code} 198 | 199 | 200 | class MessageGossipNotification(MessageGossip): 201 | """ 202 | Message that is sent from gossip to api clients which was received from other peers 203 | """ 204 | 205 | def __init__(self, data): 206 | """ 207 | C'Tor 208 | 209 | :param bytes data: the data from this message 210 | """ 211 | super().__init__(MESSAGE_CODE_NOTIFICATION, data) 212 | msg_id_fst, msg_id_snd = struct.unpack('{}B'.format(2), data[:2]) 213 | self.msg_id = bytes_to_short(msg_id_fst, msg_id_snd) 214 | data_type_fst, data_type_snd = struct.unpack('{}B'.format(2), data[2:4]) 215 | self.data_type = bytes_to_short(data_type_fst, data_type_snd) 216 | self.msg = struct.unpack('{}B'.format(len(data) - 4), data[4:len(data)]) 217 | 218 | def get_values(self): 219 | """ 220 | Method by which the values of this message are retrieved 221 | 222 | :return: a dictionary with the values of this message (keys: code, type, id, message) 223 | """ 224 | return {'id': self.msg_id, 'type': self.data_type, 'code': self.code, 'message': self.msg} 225 | 226 | 227 | class MessageGossipValidation(MessageGossip): 228 | """ 229 | Message that is sent from gossip to api clients to know whether or not a received message is valid 230 | """ 231 | 232 | def __init__(self, data): 233 | """ 234 | C'Tor 235 | 236 | :param data: the data from this message 237 | """ 238 | super().__init__(MESSAGE_CODE_VALIDATION, data) 239 | msg_id_fst, msg_id_snd = struct.unpack('{}B'.format(2), data[:2]) 240 | self.msg_id = bytes_to_short(msg_id_fst, msg_id_snd) 241 | _, valid = struct.unpack('{}B'.format(2), data[2:4]) 242 | if valid == 0: 243 | self.valid = False 244 | else: 245 | self.valid = True 246 | 247 | def get_values(self): 248 | """ 249 | Method by which the values of this message are retrieved 250 | 251 | :return: a dictionary with the values of this message (keys: id, valid, code) 252 | """ 253 | return {'id': self.msg_id, 'valid': self.valid, 'code': self.code} 254 | 255 | 256 | class MessageGossipPeerRequest(MessageGossip51x): 257 | """ 258 | Message that is sent from one peer to another to get addresses of peers the other peer is connected to 259 | """ 260 | 261 | def __init__(self, data): 262 | """ 263 | C'Tor 264 | 265 | :param data: the data from this message 266 | """ 267 | super().__init__(MESSAGE_CODE_PEER_REQUEST, data) 268 | self.data = data 269 | ipv4_part_1 = int(self.data[0]) 270 | ipv4_part_2 = int(self.data[1]) 271 | ipv4_part_3 = int(self.data[2]) 272 | ipv4_part_4 = int(self.data[3]) 273 | port = bytes_to_short(self.data[4], self.data[5]) 274 | self.address = '%s.%s.%s.%s:%s' % (ipv4_part_1, ipv4_part_2, ipv4_part_3, ipv4_part_4, port) 275 | 276 | def get_values(self): 277 | return {'code': MESSAGE_CODE_PEER_REQUEST, 'p2p_server_address': self.address} 278 | 279 | 280 | class MessageGossipPeerUpdate(MessageGossip51x): 281 | """ 282 | Message that is sent recursively through the network as a respone to a new established connection 283 | """ 284 | 285 | def __init__(self, data): 286 | """ 287 | C'Tor 288 | 289 | :param data: the data from this message 290 | """ 291 | super().__init__(MESSAGE_CODE_PEER_UPDATE, data) 292 | self.data = data 293 | ipv4_part_1 = int(self.data[0]) 294 | ipv4_part_2 = int(self.data[1]) 295 | ipv4_part_3 = int(self.data[2]) 296 | ipv4_part_4 = int(self.data[3]) 297 | port = bytes_to_short(self.data[4], self.data[5]) 298 | self.address = '%s.%s.%s.%s:%s' % (ipv4_part_1, ipv4_part_2, ipv4_part_3, ipv4_part_4, port) 299 | self.ttl = int(self.data[6]) 300 | self.update_type = int(self.data[7]) 301 | 302 | def get_values(self): 303 | """ 304 | Method by which the values of this message are retrieved 305 | 306 | :return: a dictionary with the values of this message (keys: code, address, ttl, update_type) 307 | """ 308 | return {'code': MESSAGE_CODE_PEER_UPDATE, 'address': self.address, 'ttl': self.ttl, 309 | 'update_type': self.update_type} 310 | 311 | def __hash__(self): 312 | return hash((self.address, self.update_type)) 313 | 314 | def __eq__(self, other): 315 | return (self.address, self.update_type) == (other.address, other.update_type) 316 | 317 | def __ne__(self, other): 318 | return not (self == other) 319 | 320 | 321 | class MessageGossipPeerResponse(MessageGossip51x): 322 | """ 323 | Message that is sent as an answer to a previously sent peer request. 324 | It contains a list of server identifiers (address:port) 325 | """ 326 | 327 | def __init__(self, data): 328 | """ 329 | C'Tor 330 | 331 | :param data: the data from this message 332 | """ 333 | super().__init__(MESSAGE_CODE_PEER_RESPONSE, data) 334 | self.data = data 335 | self.connections = [] 336 | if len(self.data) > 0: 337 | data = struct.unpack('{}B'.format(len(self.data)), self.data) 338 | for i in range(0, len(self.data), 6): 339 | ipv4_part_1 = int(data[i]) 340 | ipv4_part_2 = int(data[i + 1]) 341 | ipv4_part_3 = int(data[i + 2]) 342 | ipv4_part_4 = int(data[i + 3]) 343 | port = bytes_to_short(self.data[i + 4], self.data[i + 5]) 344 | address = '%s.%s.%s.%s:%s' % (ipv4_part_1, ipv4_part_2, ipv4_part_3, ipv4_part_4, port) 345 | self.connections.append(address) 346 | 347 | def get_values(self): 348 | """ 349 | Method by which the values of this message are retrieved 350 | 351 | :return: a dictionary with the values of this message (keys: code, data) 352 | """ 353 | return {'code': MESSAGE_CODE_PEER_RESPONSE, 'data': self.connections} 354 | 355 | 356 | class MessageGossipPeerInit(MessageGossip51x): 357 | """ 358 | Initial message that is sent from the connection peer to the remote peer 359 | to inform that peer about his server adderss (especially the port) 360 | """ 361 | 362 | def __init__(self, data): 363 | """ 364 | C'Tor 365 | 366 | :param data: the data from this message 367 | """ 368 | super().__init__(MESSAGE_CODE_PEER_INIT, data) 369 | self.data = data 370 | ipv4_part_1 = int(self.data[0]) 371 | ipv4_part_2 = int(self.data[1]) 372 | ipv4_part_3 = int(self.data[2]) 373 | ipv4_part_4 = int(self.data[3]) 374 | port = bytes_to_short(self.data[4], self.data[5]) 375 | self.address = '%s.%s.%s.%s:%s' % (ipv4_part_1, ipv4_part_2, ipv4_part_3, ipv4_part_4, port) 376 | 377 | def get_values(self): 378 | """ 379 | Method by which the values of this message are retrieved 380 | 381 | :return: a dictionary with the values of this message (keys: code, p2p_server_address) 382 | """ 383 | return {'code': MESSAGE_CODE_PEER_INIT, 'p2p_server_address': self.address} 384 | 385 | 386 | """ Dictionary of all known message types within Gossip """ 387 | GOSSIP_MESSAGE_TYPES = {MESSAGE_CODE_ANNOUNCE: MessageGossipAnnounce, 388 | MESSAGE_CODE_NOTIFY: MessageGossipNotify, 389 | MESSAGE_CODE_NOTIFICATION: MessageGossipNotification, 390 | MESSAGE_CODE_VALIDATION: MessageGossipValidation, 391 | MESSAGE_CODE_PEER_REQUEST: MessageGossipPeerRequest, 392 | MESSAGE_CODE_PEER_RESPONSE: MessageGossipPeerResponse, 393 | MESSAGE_CODE_PEER_UPDATE: MessageGossipPeerUpdate, 394 | MESSAGE_CODE_PEER_INIT: MessageGossipPeerInit} 395 | -------------------------------------------------------------------------------- /gossip/util/message_code.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | __author__ = 'Anselm Binninger, Thomas Maier, Ralph Schaumann' 16 | 17 | MESSAGE_CODE_ANNOUNCE = 500 18 | MESSAGE_CODE_NOTIFY = 501 19 | MESSAGE_CODE_NOTIFICATION = 502 20 | MESSAGE_CODE_VALIDATION = 503 21 | 22 | MESSAGE_CODE_PEER_REQUEST = 510 23 | MESSAGE_CODE_PEER_RESPONSE = 511 24 | MESSAGE_CODE_PEER_UPDATE = 512 25 | MESSAGE_CODE_PEER_INIT = 513 26 | 27 | MESSAGE_CODE_GOSSIP_MIN = 500 28 | MESSAGE_CODE_GOSSIP_MAX = 520 29 | -------------------------------------------------------------------------------- /gossip/util/packing.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import logging 16 | import struct 17 | 18 | from gossip.util.exceptions import GossipMessageException, GossipClientDisconnectedException 19 | from gossip.util.message_code import MESSAGE_CODE_ANNOUNCE, MESSAGE_CODE_PEER_REQUEST, MESSAGE_CODE_PEER_RESPONSE, \ 20 | MESSAGE_CODE_NOTIFICATION, MESSAGE_CODE_NOTIFY, MESSAGE_CODE_PEER_UPDATE, MESSAGE_CODE_VALIDATION, \ 21 | MESSAGE_CODE_GOSSIP_MAX, MESSAGE_CODE_GOSSIP_MIN, MESSAGE_CODE_PEER_INIT 22 | from gossip.util.byte_formatting import bytes_to_short, short_to_bytes 23 | 24 | __author__ = 'Anselm Binninger, Thomas Maier, Ralph Schaumann' 25 | 26 | 27 | def pack_gossip_announce(ttl, data_type, msg_data): 28 | """ 29 | Method by which a message of type 'GOSSIP ANNOUNCE' is packed/encoded 30 | 31 | :param ttl: the time to live 32 | :param data_type: the data type of the message 33 | :param msg_data: the data to pack 34 | :return: dict, code and data 35 | """ 36 | if ttl >= 0xff: 37 | raise ValueError('TTL may not be larger than 1 byte') 38 | # encode the ttl 39 | b_ttl = bytes([ttl]) 40 | # reserved byte 41 | b_reserved = bytes([0]) 42 | b_type = short_to_bytes(data_type) 43 | data = b'' 44 | for i in list(msg_data): 45 | data += bytes([int(i)]) 46 | return {'code': MESSAGE_CODE_ANNOUNCE, 'data': b_ttl + b_reserved + b_type + data} 47 | 48 | 49 | def pack_gossip_notify(data_type): 50 | """ 51 | Method by which a message of type 'GOSSIP NOTIFY' is packed/encoded 52 | 53 | :param data_type: the data type of the message 54 | :return: dict, code and data 55 | """ 56 | # reserved byte 57 | b_reserved = bytes([0]) 58 | b_type = short_to_bytes(data_type) 59 | return {'code': MESSAGE_CODE_NOTIFY, 'data': b_reserved + b_reserved + b_type} 60 | 61 | 62 | def pack_gossip_notification(msg_id, data_type, msg_data): 63 | """ 64 | Method by which a message of type 'GOSSIP NOTIFICATION' is packed/encoded 65 | 66 | :param msg_id: the id of the message 67 | :param data_type: the data type of the message 68 | :param msg_data: the data to pack 69 | :return: dict, code and data 70 | """ 71 | b_type = short_to_bytes(data_type) 72 | b_msg_id = short_to_bytes(msg_id) 73 | 74 | data = b'' 75 | for i in list(msg_data): 76 | data += bytes([int(i)]) 77 | return {'code': MESSAGE_CODE_NOTIFICATION, 'data': b_msg_id + b_type + data} 78 | 79 | 80 | def pack_gossip_validation(msg_id, valid_bit): 81 | """ 82 | Method by which a message of type 'GOSSIP VALIDATION' is packed/encoded 83 | 84 | :param msg_id: the id of the message 85 | :param valid_bit: set iff valid, unset else 86 | value may be 0 or 1 87 | :return: dict, code and data 88 | """ 89 | b_msg_id = short_to_bytes(msg_id) 90 | if valid_bit == 0: 91 | b_valid_bit = b'\x00' 92 | elif valid_bit == 1: 93 | b_valid_bit = b'\x01' 94 | else: 95 | raise ValueError('Valid bit may only be 1 or 0') 96 | return {'code': MESSAGE_CODE_VALIDATION, 'data': b_msg_id + bytes([0]) + b_valid_bit} 97 | 98 | 99 | def pack_gossip_peer_request(address_port): 100 | """ 101 | Method by which a message of type MESSAGE_CODE_PEER_REQUEST is packed/encoded 102 | 103 | :return: dict, code and data 104 | """ 105 | address, port = address_port.split(':') 106 | ipv4_part_1, ipv4_part_2, ipv4_part_3, ipv4_part_4 = address.split('.') 107 | b_address = bytes([int(ipv4_part_1)]) + bytes([int(ipv4_part_2)]) + bytes([int(ipv4_part_3)]) + bytes( 108 | [int(ipv4_part_4)]) 109 | b_port = short_to_bytes(port) 110 | b_reserved = b'\x00' 111 | return {'code': MESSAGE_CODE_PEER_REQUEST, 'data': b_address + b_port + b_reserved + b_reserved} 112 | 113 | 114 | def pack_gossip_peer_response(local_connections): 115 | """ 116 | Method by which a message of type MESSAGE_CODE_PEER_RESPONSE is packed/encoded 117 | 118 | :param local_connections: list with all local connections 119 | :return: dict with format {'code': , 'data': } 120 | """ 121 | b_connections = b'' 122 | for key in local_connections: 123 | address, port = key.split(':') 124 | ipv4_part_1, ipv4_part_2, ipv4_part_3, ipv4_part_4 = address.split('.') 125 | b_address = bytes([int(ipv4_part_1)]) + bytes([int(ipv4_part_2)]) + bytes([int(ipv4_part_3)]) + bytes( 126 | [int(ipv4_part_4)]) 127 | b_port = short_to_bytes(port) 128 | b_connections += b_address + b_port 129 | 130 | return {'code': MESSAGE_CODE_PEER_RESPONSE, 'data': b_connections} 131 | 132 | 133 | PEER_UPDATE_TYPE_PEER_LOST = 0 134 | PEER_UPDATE_TYPE_PEER_FOUND = 1 135 | 136 | 137 | def pack_gossip_peer_update(identifier, ttl, update_type): 138 | """ 139 | Method by which a message of type MESSAGE_CODE_PEER_UPDATE is packed/encoded 140 | 141 | :return: dict, code and data 142 | """ 143 | address, port = identifier.split(':') 144 | ipv4_part_1, ipv4_part_2, ipv4_part_3, ipv4_part_4 = address.split('.') 145 | b_address = bytes([int(ipv4_part_1)]) + bytes([int(ipv4_part_2)]) + bytes([int(ipv4_part_3)]) + bytes( 146 | [int(ipv4_part_4)]) 147 | b_port = short_to_bytes(port) 148 | b_ttl = bytes([(int(ttl) & 0xff)]) 149 | if update_type == PEER_UPDATE_TYPE_PEER_LOST: 150 | b_update = b'\x00' 151 | elif update_type == PEER_UPDATE_TYPE_PEER_FOUND: 152 | b_update = b'\x01' 153 | else: 154 | raise ValueError('update type may only be 0 or 1') 155 | return {'code': MESSAGE_CODE_PEER_UPDATE, 'data': b_address + b_port + b_ttl + b_update} 156 | 157 | 158 | def pack_gossip_peer_init(identifier): 159 | """ 160 | Method by which a message of type MESSAGE_CODE_PEER_INIT is packed/encoded 161 | 162 | :return: dict, code and data 163 | """ 164 | address, port = identifier.split(':') 165 | ipv4_part_1, ipv4_part_2, ipv4_part_3, ipv4_part_4 = address.split('.') 166 | b_address = bytes([int(ipv4_part_1)]) + bytes([int(ipv4_part_2)]) + bytes([int(ipv4_part_3)]) + bytes( 167 | [int(ipv4_part_4)]) 168 | b_port = short_to_bytes(port) 169 | 170 | return {'code': MESSAGE_CODE_PEER_INIT, 'data': b_address + b_port} 171 | 172 | 173 | def pack_message_other(code, data): 174 | """ 175 | Method by which other modules messages are (re-)packed 176 | 177 | :param code: the code of the message 178 | :param data: the data to pack 179 | :return: dict, code and data 180 | """ 181 | b_data = bytes(data, 'ascii') 182 | return {'code': code, 'data': b_data} 183 | 184 | 185 | def send_msg(sock, code, msg): 186 | """ 187 | Method by which a Message is encoded and sent 188 | 189 | :param sock: the socket to send the message on 190 | :param code: the code of the message 191 | :param msg: the message to encode 192 | """ 193 | # the size of the message is size of the code to send + 4 bytes 194 | size = len(msg) + 4 195 | # encode the size into two bytes 196 | b_size = short_to_bytes(size) 197 | # encode the message code into two bytes 198 | b_code = short_to_bytes(code) 199 | # add all the bytes up to construct the message 200 | b_msg = b_size + b_code + msg 201 | # send everything 202 | logging.info('Send message: %d | %d | %s' % (size, code, msg)) 203 | sock.send(b_msg) 204 | 205 | 206 | def receive_msg(sock): 207 | """ 208 | Method by which a byte message is decoded 209 | 210 | :param sock: tcp socket to read from 211 | :return: a dict of the decoded values 212 | """ 213 | msg_hdr = sock.recv(4) 214 | if len(msg_hdr) is 0: 215 | raise GossipClientDisconnectedException('Client disconnected') 216 | elif len(msg_hdr) < 4: 217 | raise GossipMessageException('Invalid header (< 4)') 218 | # the first and the second byte encode the message length 219 | size_fst, size_snd = struct.unpack('{}B'.format(2), msg_hdr[:2]) 220 | size = bytes_to_short(size_fst, size_snd) 221 | if size < 4: 222 | raise GossipMessageException('Invalid size (< 4)') 223 | # third and forth byte encode the message code 224 | msg_code_fst, msg_code_snd = struct.unpack('{}B'.format(2), msg_hdr[2:4]) 225 | code = bytes_to_short(msg_code_fst, msg_code_snd) 226 | if not MESSAGE_CODE_GOSSIP_MIN <= code < MESSAGE_CODE_GOSSIP_MAX: 227 | raise GossipMessageException('Invalid message code') 228 | data = sock.recv(size - 4) 229 | logging.info('Received message: %d | %d | %s' % (size, code, data)) 230 | msg = {'size': size, 'code': code, 'message': data} 231 | return msg 232 | -------------------------------------------------------------------------------- /gossip/util/queue_item_types.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | __author__ = 'Anselm Binninger, Thomas Maier, Ralph Schaumann' 16 | 17 | QUEUE_ITEM_TYPE_SEND_MESSAGE = 0 18 | QUEUE_ITEM_TYPE_RECEIVED_MESSAGE = 1 19 | QUEUE_ITEM_TYPE_ESTABLISH_CONNECTION = 2 20 | QUEUE_ITEM_TYPE_CONNECTION_LOST = 3 21 | QUEUE_ITEM_TYPE_NEW_CONNECTION = 4 22 | 23 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from gossip import gossip_main 16 | 17 | __author__ = 'Anselm Binninger, Thomas Maier, Ralph Schaumann' 18 | 19 | if __name__ == '__main__': 20 | gossip_main.main() 21 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx==1.4.1 2 | pytest==2.9.1 3 | pytest-html==1.8.0 4 | sphinx-rtd-theme==0.1.9 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the 'License'); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an 'AS IS' BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from setuptools import setup, find_packages 16 | 17 | 18 | with open('README.rst') as f: 19 | readme = f.read() 20 | 21 | with open('LICENSE') as f: 22 | license = f.read() 23 | 24 | setup( 25 | name='gossip-python', 26 | version='0.0.1', 27 | description='Python module for gossip integration', 28 | long_description=readme, 29 | author='Anselm Binninger | Ralph Oliver Schaumann | Thomas Maier', 30 | author_email='anselm.binninger@tum.de | tom.maier@tum.de', 31 | url='https://stash.bwk-technik.de/projects/PTP/repos/p2p-code/', 32 | download_url='https://bwk-software.com/builds/gossip/downloads/0.0.1', 33 | keywords=['gossip','gossip-python','peertopeer','p2p'], 34 | license=license, 35 | packages=find_packages(exclude=('tests', 'docs')), 36 | data_files=[('', ['LICENSE'])] 37 | ) -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | __author__ = 'Anselm Binninger, Thomas Maier, Ralph Schaumann' 16 | -------------------------------------------------------------------------------- /tests/communication/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | __author__ = 'Anselm Binninger, Thomas Maier, Ralph Schaumann' 16 | -------------------------------------------------------------------------------- /tests/communication/test_connection_pool.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import unittest 16 | 17 | from gossip.communication.connection import GossipConnectionPool 18 | 19 | __author__ = 'Anselm Binninger, Thomas Maier, Ralph Schaumann' 20 | 21 | 22 | class MockedConnection: 23 | def __init__(self, name): 24 | self.name = name 25 | 26 | def close(self): 27 | pass 28 | 29 | def shutdown(self, arg): 30 | pass 31 | 32 | 33 | class TestConnectionPool(unittest.TestCase): 34 | """ 35 | Test class for ConnectionPool class 36 | """ 37 | 38 | def test_maintain_connections_list(self): 39 | """ 40 | This test method adds up to four connections to a pool with a maximum size of 3 41 | It fails if after adding the fourth connection, the connection pool is larger than 3 and 42 | if any other connection, instead of the first one that has been added, was removed 43 | :return: None 44 | """ 45 | max_pool_size = 3 46 | connection_list = GossipConnectionPool('TestPool', max_pool_size) 47 | 48 | connection_list.add_connection('127.0.0.1:2', MockedConnection('DummyConnection2')) 49 | connection_list.add_connection('127.0.0.1:1', MockedConnection('DummyConnection1')) 50 | 51 | pool_size = len(connection_list._connections) 52 | assert pool_size == 2, "expected pool size to be %s but was %s" % (max_pool_size, pool_size) 53 | 54 | connection_list.add_connection('127.0.0.1:3', MockedConnection('DummyConnection3')) 55 | connection_list.add_connection('127.0.0.1:0', MockedConnection('DummyConnection0')) 56 | 57 | pool_size = len(connection_list._connections) 58 | assert pool_size == max_pool_size, "expected pool size to be %s but was %s" % (max_pool_size, pool_size) 59 | 60 | -------------------------------------------------------------------------------- /tests/control/test_api_registrations.py: -------------------------------------------------------------------------------- 1 | from control.api_registrations import APIRegistrationHandler 2 | 3 | __author__ = '' 4 | 5 | 6 | def test_api_registrations(): 7 | 8 | registrations = APIRegistrationHandler() 9 | registrations.register(500, "192.168.1.1:7001") 10 | registrations.register(500, "192.168.1.1:7002") 11 | registrations.register(500, "192.168.1.1:7003") 12 | registrations.register(500, "192.168.1.1:7004") 13 | registrations.register(501, "192.168.1.1:7001") 14 | registrations.register(501, "192.168.1.1:7002") 15 | 16 | registrations.unregister("192.168.1.1:7002") 17 | 18 | assert "192.168.1.1:7002" not in registrations.get_registrations(500) 19 | assert "192.168.1.1:7002" not in registrations.get_registrations(501) 20 | assert "192.168.1.1:7001" in registrations.get_registrations(501) 21 | print(registrations._api_registrations) 22 | 23 | registrations.unregister("192.168.1.1:7001") 24 | registrations.unregister("192.168.1.1:7003") 25 | registrations.unregister("192.168.1.1:7004") 26 | assert len(registrations.get_registrations(501)) == 0 27 | assert len(registrations.get_registrations(501)) == 0 28 | -------------------------------------------------------------------------------- /tests/control/test_message_cache.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import unittest 16 | 17 | from gossip.control.message_cache import GossipMessageCache 18 | 19 | __author__ = 'Anselm Binninger, Thomas Maier, Ralph Schaumann' 20 | 21 | 22 | class TestMessageCache(unittest.TestCase): 23 | """ 24 | Test class for GossipMessageCache class 25 | """ 26 | 27 | def test_maintain_message_cache(self): 28 | """ 29 | This test method adds up to four messages to a pool with a maximum size of 3 30 | It fails if after adding the fourth message, the message pool is larger than 3 and 31 | if any other message, instead of the first one that has been added, was removed 32 | :return: None 33 | """ 34 | max_pool_size = 3 35 | message_cache = GossipMessageCache('TestCache1', max_pool_size) 36 | 37 | id1 = message_cache.add_message("Msg1") 38 | id2 = message_cache.add_message("Msg2") 39 | 40 | cache_size = len(message_cache._msg_cache) 41 | assert cache_size == 2, "expected cache_size to be %s but was %s" % (max_pool_size, cache_size) 42 | 43 | id3 = message_cache.add_message("Msg3") 44 | id4 = message_cache.add_message("Msg4") 45 | 46 | cache_size = len(message_cache._msg_cache) 47 | assert cache_size == max_pool_size, "expected pool size to be %s but was %s" % (max_pool_size, cache_size) 48 | 49 | assert not message_cache.get_message(id1), "Expected first message added to be deleted but wasn't" 50 | assert message_cache.get_message(id2), "Expected second message added to be deleted but wasn't" 51 | assert message_cache.get_message(id3), "Expected third message added to be deleted but wasn't" 52 | assert message_cache.get_message(id4), "Expected fourth message added to be deleted but wasn't" 53 | -------------------------------------------------------------------------------- /tests/first_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | import sys 17 | 18 | __author__ = 'Anselm Binninger, Thomas Maier, Ralph Schaumann' 19 | 20 | myPath = os.path.dirname(os.path.abspath(__file__)) 21 | sys.path.insert(0, myPath + '/../gossip') 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/util/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | __author__ = 'Anselm Binninger, Thomas Maier, Ralph Schaumann' 16 | -------------------------------------------------------------------------------- /tests/util/test_byte_formatting.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from gossip.util import byte_formatting 16 | 17 | __author__ = 'Anselm Binninger, Thomas Maier, Ralph Schaumann' 18 | 19 | 20 | def test_short_to_bytes(): 21 | short_first = 65535 22 | short_second = 0 23 | short_third = 3999 24 | short_fourth = 255 25 | sf_bytes = byte_formatting.short_to_bytes(short_first) 26 | assert sf_bytes[0] == 0xff 27 | assert sf_bytes[1] == 0xff 28 | 29 | ss_bytes = byte_formatting.short_to_bytes(short_second) 30 | assert ss_bytes[0] == 0x00 31 | assert ss_bytes[1] == 0x00 32 | 33 | st_bytes = byte_formatting.short_to_bytes(short_third) 34 | assert st_bytes[0] == 0x0F 35 | assert st_bytes[1] == 0x9F 36 | 37 | sfo_bytes = byte_formatting.short_to_bytes(short_fourth) 38 | assert sfo_bytes[0] == 0x00 39 | assert sfo_bytes[1] == 0xff 40 | 41 | 42 | def test_bytes_to_short(): 43 | short_first = byte_formatting.bytes_to_short(0xff, 0xff) 44 | short_second = byte_formatting.bytes_to_short(0x00, 0x00) 45 | short_third = byte_formatting.bytes_to_short(0x0f, 0x9f) 46 | short_fourth = byte_formatting.bytes_to_short(0x00, 0xff) 47 | assert short_first == 65535 48 | assert short_second == 0 49 | assert short_third == 3999 50 | assert short_fourth == 255 -------------------------------------------------------------------------------- /tests/util/test_message.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Anselm Binninger, Thomas Maier, Ralph Schaumann 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import unittest 16 | 17 | __author__ = 'Anselm Binninger, Thomas Maier, Ralph Schaumann' 18 | 19 | 20 | class TestMessageGossipAnnounce(unittest.TestCase): 21 | """ 22 | Test class for MessageGossipAnnounce class 23 | """ 24 | 25 | def test_encode_and_decode_of_message(self): 26 | """ 27 | Tests if a MessageGossipAnnounce can be encoded and properly decoded 28 | :return: None 29 | """ 30 | # values = packing.pack_gossip_announce(0, 540, 'p2p is very cool!') 31 | # msg_500 = message.MessageGossipAnnounce(values['data']) 32 | # msg_500_enc = msg_500.encode() 33 | # msg_500_dec = message.MessageGossipAnnounce(data=msg_500_enc[4:]) 34 | # assert msg_500_dec.code == msg_500.code, "expected %s but was %s" % (msg_500_dec.code, msg_500.code) 35 | # assert msg_500_dec.data == msg_500.data, "expected %s but was %s" % (msg_500_dec.data, msg_500.data) 36 | pass 37 | 38 | class TestMessageGossipNotify(unittest.TestCase): 39 | """ 40 | Test class for MessageGossipNotify class 41 | """ 42 | 43 | def test_encode_and_decode_of_message(self): 44 | """ 45 | Tests if a MessageGossipNotify can be encoded and properly decoded 46 | :return: None 47 | """ 48 | # values = packing.pack_gossip_notify(540) 49 | # msg = message.MessageGossipNotify(values['data']) 50 | # msg_enc = msg.encode() 51 | # msg_501 = message.MessageGossipNotify(values['data']) 52 | # msg_501_enc = msg_501.encode() 53 | # msg_501_dec = message.MessageGossipNotify(data=msg_501_enc[4:]) 54 | # assert msg_501_dec.code == msg_501.code, "expected %s but was %s" % (msg_501_dec.code, msg_501.code) 55 | # assert msg_501_dec.data == msg_501.data, "expected %s but was %s" % (msg_501_dec.data, msg_501.data) 56 | pass 57 | --------------------------------------------------------------------------------