├── .gitignore ├── COPYING ├── Dockerfile ├── EXAMPLE ├── README.md ├── consensus ├── configure-test.py ├── consensus-client │ ├── consensus_client │ │ ├── __init__.py │ │ ├── canonical.py │ │ ├── client.py │ │ ├── config.py │ │ ├── utils.py │ │ └── zeus_events.py │ ├── requirements.txt │ └── setup.py ├── consensus-service │ ├── consensus_django │ │ ├── __init__.py │ │ ├── settings.py │ │ ├── spec.py │ │ ├── test_settings.py │ │ ├── urls.py │ │ └── wsgi.py │ ├── consensus_service │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── logic.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── rules.py │ │ ├── tests.py │ │ └── views.py │ ├── manage.py │ ├── requirements.txt │ ├── requirements_dev.txt │ └── setup.py ├── pytest.ini ├── run-functional-tests.sh ├── run-unit-tests.sh └── tests │ ├── client │ ├── run.sh │ └── test.py │ └── events │ ├── config │ ├── admin │ ├── election.zeus │ └── trustee │ ├── run.sh │ └── test.py ├── demo ├── README ├── cli_functions.bash ├── conf │ ├── ec1_settings │ ├── ec2_settings │ ├── gpg_server.conf │ ├── key1 │ ├── key2 │ ├── sphinxmix_server.conf │ ├── user1_settings │ ├── user2_settings │ ├── zeus1 │ ├── zeus1_keyid │ ├── zeus2 │ ├── zeus2_keyid │ ├── zeus_joined │ ├── zeus_joined_keyid │ └── zeus_server.conf ├── demo-gpg.sh ├── demo-sphinxmix.sh ├── demo-zeus.sh ├── init.sh ├── launch-gpg.sh ├── launch-sphinxmix.sh ├── launch-zeus.sh ├── mixnet_functions.bash ├── peer1.data │ ├── zeus │ └── zeus_keyid └── peer2.data │ ├── zeus │ └── zeus_keyid ├── docker ├── README.rst ├── create-dev.sh ├── init.sh ├── inspect.sh ├── package.json ├── requirements-preheat.txt ├── restart.sh ├── services.conf └── view-logs.sh ├── docs ├── Makefile ├── conf.py ├── example_requests.py ├── index.rst ├── mixnet_setup.rst └── panoramix_api.rst ├── panoramix-agent ├── panoramix │ ├── __init__.py │ └── agent │ │ ├── __init__.py │ │ ├── agent.py │ │ ├── client.py │ │ └── sphinxmix_agent.py ├── requirements.txt └── setup.py ├── panoramix-service ├── panoramix_django │ ├── __init__.py │ ├── manage.py │ ├── settings.py │ ├── spec.py │ ├── test_settings.py │ ├── urls.py │ └── wsgi.py ├── panoramix_service │ ├── __init__.py │ ├── apps.py │ ├── logic.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ └── rules.py ├── pytest.ini ├── requirements.txt ├── setup.py └── test.py ├── panoramix ├── panoramix │ ├── __init__.py │ ├── backends │ │ ├── __init__.py │ │ ├── gpg_backend.py │ │ ├── sphinxmix_backend.py │ │ ├── zeus_backend.py │ │ └── zeus_crypto.py │ ├── canonical.py │ ├── common.py │ ├── config.py │ ├── interface.py │ ├── ui_term.py │ ├── ui_web.py │ ├── utils.py │ └── wizard.py ├── requirements.txt ├── requirements_gpg.txt ├── requirements_sphinxmix.txt ├── requirements_zeus.txt └── setup.py ├── run_test.sh ├── test └── mixing │ ├── config │ ├── admin │ ├── client │ ├── contributor │ └── panoramix_db.sqlite3 │ ├── mixing.py │ ├── run-admin.sh │ ├── run-client-daemon.sh │ ├── run-contrib.sh │ ├── run-service.sh │ ├── run.sh │ └── shell.sh ├── trustpanel ├── requirements.txt ├── setup.py └── trustpanel │ ├── __init__.py │ └── base.py ├── ui ├── .editorconfig ├── .ember-cli ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .template-lintrc.js ├── .travis.yml ├── .watchmanconfig ├── README.md ├── app │ ├── app.js │ ├── components │ │ ├── .gitkeep │ │ ├── election-view │ │ │ ├── component.js │ │ │ └── template.hbs │ │ ├── paper-item-value │ │ │ ├── component.js │ │ │ └── template.hbs │ │ ├── stage-key-view │ │ │ ├── component.js │ │ │ └── template.hbs │ │ └── stage-view │ │ │ ├── component.js │ │ │ └── template.hbs │ ├── controllers │ │ ├── .gitkeep │ │ ├── application.js │ │ └── election.js │ ├── helpers │ │ └── .gitkeep │ ├── index.html │ ├── models │ │ └── .gitkeep │ ├── resolver.js │ ├── router.js │ ├── routes │ │ ├── .gitkeep │ │ ├── election-with-id.js │ │ ├── election.js │ │ └── index.js │ ├── services │ │ └── api.js │ ├── styles │ │ ├── _contrast.scss │ │ ├── app.css │ │ └── app.scss │ └── templates │ │ ├── application.hbs │ │ ├── components │ │ ├── .gitkeep │ │ └── election-tree-view.hbs │ │ └── election.hbs ├── bower.json ├── config │ ├── environment.js │ ├── optional-features.json │ └── targets.js ├── ember-cli-build.js ├── lib │ └── apimas-docs │ │ ├── addon │ │ ├── components │ │ │ ├── doc-item-value │ │ │ │ ├── component.js │ │ │ │ └── template.hbs │ │ │ ├── doc-view-item │ │ │ │ ├── component.js │ │ │ │ └── template.hbs │ │ │ └── doc-view │ │ │ │ ├── component.js │ │ │ │ └── template.hbs │ │ ├── helpers │ │ │ └── doc-params.js │ │ └── utils.js │ │ ├── app │ │ ├── components │ │ │ ├── doc-item-value │ │ │ │ └── component.js │ │ │ ├── doc-view-item │ │ │ │ └── component.js │ │ │ └── doc-view │ │ │ │ └── component.js │ │ └── helpers │ │ │ └── doc-params.js │ │ ├── index.js │ │ └── package.json ├── package-lock.json ├── package.json ├── public │ └── robots.txt ├── testem.js ├── tests │ ├── helpers │ │ └── .gitkeep │ ├── index.html │ └── test-helper.js ├── vendor │ └── .gitkeep └── yarn.lock ├── version.txt └── zeus_trust ├── __init__.py ├── options.py ├── spec.py └── trust.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:jessie 2 | MAINTAINER Kostas Papadimitriou "kpap@grnet.gr" 3 | 4 | RUN find /var/lib/apt -type f -exec rm {} \+ 5 | RUN apt-get -y update 6 | RUN apt-get -y install vim git lsb-release wget multitail python python-pip build-essential \ 7 | python-dev libgpm2 libgmp-dev libxml2 libxml2-dev libxslt1-dev curl \ 8 | supervisor 9 | RUN curl -sL https://deb.nodesource.com/setup_11.x | bash - 10 | RUN apt-get install -y nodejs 11 | RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - 12 | RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list 13 | RUN apt-get update && apt-get install -y yarn 14 | 15 | # preheat yarn 16 | ADD docker/package.json /tmp/preheat-npm/package.json 17 | RUN cd /tmp/preheat-npm && yarn install 18 | RUN yarn global add bower ember-cli 19 | 20 | # preheat pip install 21 | ADD docker/requirements-preheat.txt /srv/requirements-preheat.txt 22 | RUN pip install -r /srv/requirements-preheat.txt 23 | 24 | # dev stuff 25 | RUN apt-get -y install vim vim-ctrlp vim-python-jedi vim-scripts vim-tlib zsh 26 | RUN apt-get -y install apt-utils curl gunicorn 27 | RUN sh -c "$(curl -fsSL https://raw.github.com/robbyrussell/oh-my-zsh/master/tools/install.sh)" 28 | 29 | RUN mkdir -p /srv/ 30 | 31 | # ui 32 | ADD ui /srv/ui 33 | RUN cd /srv/ui && yarn install 34 | RUN cd /srv/ui && bower --allow-root install 35 | RUN cd /srv/ui && ember build 36 | 37 | ADD . /srv/app 38 | 39 | # consnesus 40 | RUN cd /srv/app/consensus/consensus-client && python setup.py develop 41 | RUN cd /srv/app/consensus/consensus-service && python setup.py develop 42 | 43 | # trustpanel 44 | RUN cd /srv/app/trustpanel && python setup.py develop 45 | 46 | ADD docker/services.conf /srv/ 47 | RUN mkdir /srv/db 48 | RUN mkdir /srv/logs 49 | 50 | RUN pip install supervisor-stdout 51 | 52 | ENV CONSENSUS_DATABASE /srv/db/sqlite.db 53 | ENV NTRUSTEES 2 54 | ENV UI_DIR /srv/ui 55 | ENV PORT 9000 56 | 57 | WORKDIR /srv/app 58 | 59 | ADD docker/init.sh /srv/init.sh 60 | CMD /srv/init.sh 61 | -------------------------------------------------------------------------------- /EXAMPLE: -------------------------------------------------------------------------------- 1 | Example setup of a sphinxmix mixnet 2 | =================================== 3 | 4 | For a minimal setup, let's consider five entities: the server, three sphinxmix 5 | mixers, and an end user. 6 | 7 | First set up the server database with: 8 | PANORAMIX_DATABASE= panoramix-manage migrate 9 | 10 | The database will be automatically created at the location specified by the 11 | PANORAMIX_DATABASE environment variable. 12 | 13 | Then, start the server with: 14 | PANORAMIX_DATABASE= panoramix-manage runserver --nothreading 15 | 16 | 17 | We will run the mixers and the end user in separate terminals, each with 18 | its own configuration: set environment variable PANORAMIX_CONFIG to some 19 | different file in each terminal. 20 | 21 | Run three instances of panoramix-wizard. Choose role 'admin' for the first 22 | and 'contrib' for the rest. The wizards must be run interactively. 23 | 24 | All three wizards set off by creating a crypto key and registering it to the 25 | server. The admin wizard must then collect off-line the PEER_IDs of all the 26 | mixers and register them (including its own): 27 | 28 | MIXERS: PEER_ID1,PEER_ID2,PEER_ID3 29 | 30 | The admin must then set the name of the mixnet, and the minimum number of 31 | messages per mix cycle (MIN_SIZE). This configures the mixnet, registering 32 | all related endpoints on the server. 33 | 34 | The contrib wizards must then enter the exact same values for the mixnet 35 | name and the min size as the admin. 36 | 37 | Once set up, the mixnet waits for input at the endpoint: 38 | 39 | http://127.0.0.1:8000/panoramix/endpoints// 40 | 41 | and outputs the mixed messages at the endpoint: 42 | 43 | http://127.0.0.1:8000/panoramix/endpoints/_output/ 44 | 45 | You have set up a sphinxmix network with three mixes, in a static 46 | configuration. 47 | 48 | As for the end user, in a separate terminal with its own PANORAMIX_CONFIG 49 | setting, run sphimxmix-agent and initialize it with the mixnet URL mentioned 50 | above. In another terminal with the same PANORAMIX_CONFIG as the agent, run 51 | panoramix-client and send some messages through the mixnet. 52 | 53 | You can choose any string as recipient. After you send MIN_SIZE messages, 54 | you can notice at the wizard terminals that the messages are being 55 | processed. 56 | 57 | Mixing is done in cycles. Messages that arrive at the input endpoint are 58 | marked with the current cycle. After the required number of messages is 59 | collected, the mixnet admin marks the cycle as ready for mixing and starts a 60 | new cycle. Similarly, mixers process the messages in their respective cycles. 61 | 62 | Messages are not actually sent to a recipient, but you can review the mixnet 63 | output with: 64 | 65 | panoramix-client --output 66 | 67 | You can then acknowledge retrieving the messages, so that the mixnet can 68 | safely delete them. 69 | 70 | panoramix-client --ack 71 | 72 | Cycle ids are integer serials, starting from 1. 73 | One can operate the client programmatically: 74 | 75 | PANORAMIX_CONFIG= python 76 | > from panoramix.agent import client 77 | > client.send_message(text='the text, recipient='recipient') 78 | # Use text=None, recipient=None for interactive mode 79 | 80 | > client.output_cycle(1) 81 | > client.ack_cycle(1) 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Panoramix Framework and Tools 2 | 3 | This repository includes the Panoramix Mixnet Service and the Panoramix 4 | Trust Control Panel. 5 | 6 | ## Panoramix Mixnet Service 7 | 8 | This is a framework that enables setting up and running mixnets. 9 | It employs three python packages: 10 | 11 | * panoramix: The main package that implements the mixnet logic 12 | * panoramix-service: A server implementing the panoramix API 13 | * panoramix-agent: A local agent for posting messages 14 | 15 | ### Installation 16 | 17 | In each of the three packages, run: 18 | 19 | python setup.py install 20 | 21 | The panoramix package includes three cryptographic backends: 'gpg', 'zeus', 22 | and 'sphinxmix'. In order to use them, you must install the respective 23 | dependencies with: 24 | 25 | pip install -r requirements_.txt 26 | 27 | ### Usage 28 | 29 | The service can be run with `panoramix-manage runserver --nothreading`. 30 | 31 | The panoramix package provides a wizard (panoramix-wizard) to allow the 32 | mixnet contributors to set up a mixnet (currently works with the sphinxmix 33 | backend only). 34 | 35 | The panoramix-agent package provides a local agent to enable an end user to 36 | use a mixnet. As an example, sphinxmix-agent is a wizard to configure and 37 | launch an agent for sphinxmix. The end user can send messages to the mixnet 38 | by posting it to the local agent, using the panoramix-client. 39 | 40 | ### Demo 41 | 42 | Check file EXAMPLE for an extended description on how to setup and use a 43 | sphinxmix mixnet. For a demonstration of Panoramix workflow, see 44 | demo/README. 45 | 46 | 47 | ## Panoramix Trust Control Panel 48 | 49 | This is a tool that provides a cryprographically secure and auditable 50 | mechanism for configuring applications jointly by multiple mutually 51 | untrusted authorities. 52 | 53 | ### Setup 54 | 55 | The Trust Control Panel employs three python packages: 56 | 57 | * consensus-service: A server the runs negotiations among users 58 | * consensus-client: Client for the consensus service 59 | * trustpanel: The Trust Control Panel base logic 60 | 61 | and an Ember-based web interface. 62 | 63 | Each application that wishes to employ this tool must extend the trustpanel 64 | base package. As an example, the repository includes an extension for the 65 | Zeus E-voting System (directory zeus_trust): 66 | 67 | * `trust.py` - *Initialize and launche the trust panel.* 68 | * `options.py` - *Configure the user actions for each configuration option.* 69 | * `spec.py` - *Specify the configuration options.* 70 | 71 | 72 | ### Deployment 73 | 74 | The tool can be deployed using docker. From the root directory, run: 75 | 76 | ./docker/create-dev.sh 77 | 78 | By default, the Zeus extension is installed and run. The Trust Control Panel 79 | is accessible at . 80 | 81 | Extensions for other applications must be registered in the file 82 | `docker/services.conf`. 83 | 84 | 85 | ## Panoramix Horizon 2020 86 | 87 | This project has received funding from the European Union’s Horizon 2020 88 | Research and Innovation Programme under Grant Agreement No 653497. 89 | 90 | ## Copyright and license 91 | 92 | Copyright (C) 2016-2019 GRNET S.A. 93 | 94 | This program is free software: you can redistribute it and/or modify 95 | it under the terms of the GNU Affero General Public License as 96 | published by the Free Software Foundation, either version 3 of the 97 | License, or (at your option) any later version. 98 | 99 | This program is distributed in the hope that it will be useful, 100 | but WITHOUT ANY WARRANTY; without even the implied warranty of 101 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 102 | GNU Affero General Public License for more details. 103 | 104 | You should have received a copy of the GNU Affero General Public License 105 | along with this program. If not, see . 106 | -------------------------------------------------------------------------------- /consensus/configure-test.py: -------------------------------------------------------------------------------- 1 | cd consensus-service && pip install -r requirements_dev.txt 2 | -------------------------------------------------------------------------------- /consensus/consensus-client/consensus_client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grnet/panoramix/a9b788e1a073e106d5542511094fe4ac3828d4b6/consensus/consensus-client/consensus_client/__init__.py -------------------------------------------------------------------------------- /consensus/consensus-client/consensus_client/client.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from consensus_client import utils 3 | 4 | 5 | def normalize(s): 6 | return s.rstrip('/') + '/' 7 | 8 | 9 | class Client(object): 10 | def __init__(self, endpoint, crypto_client=None): 11 | self.endpoint = normalize(endpoint) 12 | self.negotiations = self.endpoint + 'negotiations/' 13 | self.consensus = self.endpoint + 'consensus/' 14 | self.crypto_client = crypto_client 15 | 16 | def register_crypto(self, crypto_client): 17 | self.crypto_client = crypto_client 18 | 19 | def negotiation_create(self, negotiation_id=None): 20 | data = None if negotiation_id is None else {'id': negotiation_id} 21 | response = requests.post(self.negotiations, json=data) 22 | assert response.status_code == 201 23 | return response.json() 24 | 25 | def negotiation_retrieve(self, negotiation_id): 26 | path = '%s%s/' % (self.negotiations, negotiation_id) 27 | response = requests.get(path) 28 | assert response.status_code == 200 29 | return response.json() 30 | 31 | def contribution_create(self, negotiation_id, body, accept): 32 | text = utils.prepare_text(body, accept) 33 | signature = self.crypto_client.sign(text) 34 | data = { 35 | 'text': text, 36 | 'signature': signature, 37 | } 38 | path = '%s%s/contributions/' % (self.negotiations, negotiation_id) 39 | response = requests.post(path, json=data) 40 | assert response.status_code == 201 41 | return response.json() 42 | 43 | def contribution_verify(self, contribution): 44 | text = contribution['text'] 45 | signature = contribution['signature'] 46 | signer_key_id = contribution['signer_key_id'] 47 | verified_key_id = self.crypto_client.verify(signature, text) 48 | return verified_key_id and signer_key_id == verified_key_id 49 | 50 | def consensus_list(self): 51 | response = requests.get(self.consensus) 52 | assert response.status_code == 200 53 | return response.json() 54 | 55 | def consensus_retrieve(self, consensus_id): 56 | path = '%s%s/' % (self.consensus, consensus_id) 57 | response = requests.get(path) 58 | assert response.status_code == 200 59 | return response.json() 60 | -------------------------------------------------------------------------------- /consensus/consensus-client/consensus_client/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import importlib 4 | from collections import OrderedDict 5 | 6 | 7 | class Config(object): 8 | def __init__(self, config_file): 9 | self.config_file = config_file 10 | self._cfg = None 11 | 12 | def load(self): 13 | if os.path.exists(self.config_file): 14 | with open(self.config_file) as f: 15 | cfg_s = f.read() 16 | else: 17 | cfg_s = '' 18 | return json.loads(cfg_s, object_pairs_hook=OrderedDict)\ 19 | if cfg_s else OrderedDict() 20 | 21 | def reload(self): 22 | self._cfg = self.load() 23 | 24 | def cfg(self): 25 | if self._cfg is None: 26 | self._cfg = self.load() 27 | return self._cfg 28 | 29 | def get_value(self, key, default=None): 30 | value = self.cfg().get(key) 31 | return value if value is not None else default 32 | 33 | def save(self): 34 | _cfg = self._cfg 35 | if _cfg is None: 36 | return 37 | with open(self.config_file, "w") as f: 38 | json.dump(_cfg, f, indent=2) 39 | 40 | def set_value(self, key, value): 41 | cfg = self.cfg() 42 | cfg[key] = value 43 | self.save() 44 | 45 | def copy_to(self, key): 46 | return lambda value: self.set_value(key, value) 47 | 48 | def pop(self, key): 49 | cfg = self.cfg() 50 | value = cfg.pop(key, None) 51 | self.save() 52 | return value 53 | -------------------------------------------------------------------------------- /consensus/consensus-client/consensus_client/utils.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import hashlib 3 | import ecdsa 4 | from consensus_client import canonical 5 | 6 | 7 | def hash_string(s): 8 | return hashlib.sha256(s).hexdigest() 9 | 10 | 11 | def hash_with_canonical(obj): 12 | return hash_string(canonical.to_canonical(obj)) 13 | 14 | 15 | def prepare_text(body, accept): 16 | meta = {'accept': accept} 17 | text = {'body': body, 'meta': meta} 18 | return canonical.to_canonical(text) 19 | 20 | 21 | def has_accept_meta(text): 22 | unpacked_text = canonical.from_unicode_canonical(text) 23 | meta = unpacked_text.get("meta", {}) 24 | return meta.get("accept", False) 25 | 26 | 27 | def make_ecdsa_signing_key(): 28 | sk = ecdsa.SigningKey.generate() 29 | return sk.to_pem() 30 | 31 | 32 | def compute_ecdsa_key_id(vk): 33 | return hash_string(vk.to_string()) 34 | 35 | 36 | class ECDSAClient(object): 37 | def __init__(self, secret=None): 38 | self.secret = secret 39 | 40 | if self.secret: 41 | self.sk = ecdsa.SigningKey.from_pem(self.secret) 42 | vk = self.sk.get_verifying_key() 43 | self.public = vk.to_pem() 44 | self.key_id = compute_ecdsa_key_id(vk) 45 | 46 | def sign(self, text): 47 | assert self.secret 48 | sig = base64.b64encode(self.sk.sign(text)) 49 | return canonical.to_canonical({'sig': sig, 'public': self.public}) 50 | 51 | def verify(self, signature, text): 52 | signature = canonical.from_unicode_canonical(signature) 53 | sig = base64.b64decode(signature['sig']) 54 | public = signature['public'] 55 | vk = ecdsa.VerifyingKey.from_pem(public) 56 | try: 57 | assert vk.verify(sig, text) 58 | return compute_ecdsa_key_id(vk) 59 | except: 60 | return None 61 | -------------------------------------------------------------------------------- /consensus/consensus-client/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | ecdsa 3 | gmpy 4 | pycrypto 5 | -------------------------------------------------------------------------------- /consensus/consensus-client/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | CURPATH = os.path.dirname(os.path.realpath(__file__)) 5 | 6 | def get_requirements(): 7 | req_file = os.path.join(CURPATH, "requirements.txt") 8 | with open(req_file) as f: 9 | return [ 10 | x.strip('\n') 11 | for x in f.readlines() 12 | if x and x[0] != '#' 13 | ] 14 | 15 | version_file = os.path.join(CURPATH, "..", "..", "version.txt") 16 | with open(version_file) as f: 17 | version = f.read().strip() 18 | 19 | package_name = 'consensus-client' 20 | description = 'Panoramix negotiation and consensus service client' 21 | 22 | setup( 23 | name=package_name, 24 | version=version, 25 | license='Affero GPL v3', 26 | author='GRNET S.A.', 27 | author_email='panoramix@dev.grnet.gr', 28 | description=description, 29 | packages=find_packages(), 30 | install_requires=get_requirements() 31 | ) 32 | -------------------------------------------------------------------------------- /consensus/consensus-service/consensus_django/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grnet/panoramix/a9b788e1a073e106d5542511094fe4ac3828d4b6/consensus/consensus-service/consensus_django/__init__.py -------------------------------------------------------------------------------- /consensus/consensus-service/consensus_django/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for consensus_django project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.11.12. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.11/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '7l$%qf8-3u*4om&)^jqz9sqcvj_2s=2r5w+b9kyrbvo3j-398k' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'consensus_service', 41 | ] 42 | 43 | MIDDLEWARE = [ 44 | 'django.middleware.security.SecurityMiddleware', 45 | 'django.contrib.sessions.middleware.SessionMiddleware', 46 | 'django.middleware.common.CommonMiddleware', 47 | 'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.messages.middleware.MessageMiddleware', 50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 51 | ] 52 | 53 | ROOT_URLCONF = 'consensus_django.urls' 54 | 55 | TEMPLATES = [ 56 | { 57 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 58 | 'DIRS': [], 59 | 'APP_DIRS': True, 60 | 'OPTIONS': { 61 | 'context_processors': [ 62 | 'django.template.context_processors.debug', 63 | 'django.template.context_processors.request', 64 | 'django.contrib.auth.context_processors.auth', 65 | 'django.contrib.messages.context_processors.messages', 66 | ], 67 | }, 68 | }, 69 | ] 70 | 71 | WSGI_APPLICATION = 'consensus_django.wsgi.application' 72 | 73 | 74 | # Database 75 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 76 | 77 | db_path = os.environ.get( 78 | 'CONSENSUS_DATABASE', os.path.join(BASE_DIR, 'db.sqlite3')) 79 | 80 | 81 | DATABASES = { 82 | 'default': { 83 | 'ENGINE': 'django.db.backends.sqlite3', 84 | 'NAME': db_path, 85 | } 86 | } 87 | 88 | 89 | # Password validation 90 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators 91 | 92 | AUTH_PASSWORD_VALIDATORS = [ 93 | { 94 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 95 | }, 96 | { 97 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 98 | }, 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 101 | }, 102 | { 103 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 104 | }, 105 | ] 106 | 107 | 108 | # Internationalization 109 | # https://docs.djangoproject.com/en/1.11/topics/i18n/ 110 | 111 | LANGUAGE_CODE = 'en-us' 112 | 113 | TIME_ZONE = 'UTC' 114 | 115 | USE_I18N = True 116 | 117 | USE_L10N = True 118 | 119 | USE_TZ = False 120 | 121 | 122 | # Static files (CSS, JavaScript, Images) 123 | # https://docs.djangoproject.com/en/1.11/howto/static-files/ 124 | 125 | STATIC_URL = '/static/' 126 | 127 | LOGGING = { 128 | 'version': 1, 129 | 'disable_existing_loggers': False, 130 | 'handlers': { 131 | 'console': { 132 | 'class': 'logging.StreamHandler', 133 | }, 134 | }, 135 | 'loggers': { 136 | 'django': { 137 | 'handlers': ['console'], 138 | 'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'), 139 | }, 140 | }, 141 | } 142 | -------------------------------------------------------------------------------- /consensus/consensus-service/consensus_django/spec.py: -------------------------------------------------------------------------------- 1 | DEPLOY_CONFIG = { 2 | ":root_url": "http://127.0.0.1:8000/", 3 | } 4 | 5 | NEGOTIATIONS = { 6 | '.collection.django': {}, 7 | 'model': 'consensus_service.models.Negotiation', 8 | 'actions': { 9 | '.action-template.django.create': {}, 10 | '.action-template.django.retrieve': {}, 11 | }, 12 | 'fields': { 13 | 'id': { 14 | '.field.string': {}, 15 | 'default_fn': 'consensus_service.logic.generate_random_key', 16 | }, 17 | 'status': { 18 | '.field.string': {}, 19 | '.flag.nowrite': {}}, 20 | 'consensus_id': { 21 | '.field.string': {}, 22 | '.flag.nullable': {}, 23 | '.flag.nowrite': {}}, 24 | 'contributions': { 25 | '.field.collection.django': {}, 26 | '.flag.nowrite': {}, 27 | 'model': 'consensus_service.models.Contribution', 28 | 'bound': 'negotiation', 29 | 'actions': { 30 | '.action-template.django.create': {}, 31 | 'create': { 32 | ':custom_create_handler': 'consensus_service.logic.contribute', 33 | }, 34 | }, 35 | 'fields': { 36 | 'id': { 37 | '.field.serial': {}}, 38 | 'text': { 39 | '.field.text': {}}, 40 | 'latest': { 41 | '.field.boolean': {}, 42 | '.flag.nowrite': {}}, 43 | 'signer_key_id': { 44 | '.field.string': {}, 45 | '.flag.nowrite': {}}, 46 | 'signature': { 47 | '.field.text': {}}, 48 | }, 49 | }, 50 | }, 51 | } 52 | 53 | CONSENSUS = { 54 | '.collection.django': {}, 55 | 'model': 'consensus_service.models.Negotiation', 56 | 'subset': 'consensus_service.models.finished_negotiations', 57 | 'fields': { 58 | 'id': { 59 | 'source': 'consensus_id', 60 | '.field.string': {}, 61 | '.flag.nowrite': {}}, 62 | 'negotiation_id': { 63 | 'source': 'id', 64 | '.field.string': {}, 65 | '.flag.nowrite': {}}, 66 | 'text': { 67 | '.field.text': {}, 68 | '.flag.nowrite': {}}, 69 | 'timestamp': { 70 | '.field.datetime': {}, 71 | '.flag.nowrite': {}}, 72 | 'signings': { 73 | '.field.collection.django': {}, 74 | 'model': 'consensus_service.models.Signing', 75 | 'bound': 'negotiation', 76 | 'fields': { 77 | 'id': { 78 | '.field.serial': {}}, 79 | 'signer_key_id': { 80 | '.field.string': {}, 81 | '.flag.nowrite': {}}, 82 | 'signature': { 83 | '.field.text': {}, 84 | '.flag.nowrite': {}}, 85 | }, 86 | }, 87 | }, 88 | 'actions': { 89 | '.action-template.django.list': {}, 90 | '.action-template.django.retrieve': {}, 91 | }, 92 | } 93 | 94 | APP_CONFIG = { 95 | '.apimas_app': {}, 96 | ':permission_rules': 'consensus_service.rules.get_rules', 97 | 'endpoints': { 98 | 'consensus': { 99 | 'collections': { 100 | 'negotiations': NEGOTIATIONS, 101 | 'consensus': CONSENSUS, 102 | }, 103 | }, 104 | }, 105 | } 106 | -------------------------------------------------------------------------------- /consensus/consensus-service/consensus_django/test_settings.py: -------------------------------------------------------------------------------- 1 | from consensus_django.settings import * 2 | 3 | import logging 4 | logging.basicConfig(level='DEBUG') 5 | 6 | DEBUG = False 7 | DEBUG_PROPAGATE_EXCEPTIONS = True 8 | -------------------------------------------------------------------------------- /consensus/consensus-service/consensus_django/urls.py: -------------------------------------------------------------------------------- 1 | """consensus_django URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.11/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url 17 | from django.contrib import admin 18 | 19 | from apimas_django import provider 20 | from consensus_django.spec import APP_CONFIG, DEPLOY_CONFIG 21 | 22 | app_spec = provider.configure_apimas_app(APP_CONFIG) 23 | deployment_spec = provider.configure_spec(app_spec, DEPLOY_CONFIG) 24 | 25 | api_urls = provider.construct_views(deployment_spec) 26 | 27 | urlpatterns = [ 28 | url(r'^admin/', admin.site.urls), 29 | ] 30 | urlpatterns.extend(api_urls) 31 | -------------------------------------------------------------------------------- /consensus/consensus-service/consensus_django/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for consensus_django project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "consensus_django.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /consensus/consensus-service/consensus_service/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grnet/panoramix/a9b788e1a073e106d5542511094fe4ac3828d4b6/consensus/consensus-service/consensus_service/__init__.py -------------------------------------------------------------------------------- /consensus/consensus-service/consensus_service/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.contrib import admin 5 | 6 | # Register your models here. 7 | -------------------------------------------------------------------------------- /consensus/consensus-service/consensus_service/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.apps import AppConfig 5 | 6 | 7 | class ConsensusServiceConfig(AppConfig): 8 | name = 'consensus_service' 9 | -------------------------------------------------------------------------------- /consensus/consensus-service/consensus_service/logic.py: -------------------------------------------------------------------------------- 1 | import os 2 | import base64 3 | import datetime 4 | from apimas import errors 5 | from consensus_service import models 6 | from consensus_client import utils 7 | from apimas.utils import import_object 8 | 9 | get_now = datetime.datetime.utcnow 10 | 11 | client_path = os.environ.get('CONSENSUS_CRYPTO_CLIENT') 12 | crypto_client_class = import_object(client_path) if client_path \ 13 | else utils.ECDSAClient 14 | crypto_client = crypto_client_class() 15 | 16 | 17 | def generate_random_key(): 18 | s = os.urandom(32) 19 | return base64.urlsafe_b64encode(s).rstrip('=') 20 | 21 | 22 | def get_negotiation_for_update(negotiation_id): 23 | try: 24 | objects = models.Negotiation.objects.select_for_update() 25 | return objects.get(id=negotiation_id) 26 | except models.Negotiation.DoesNotExist: 27 | raise errors.NotFound("Negotiation '%s' not found." % negotiation_id) 28 | 29 | 30 | def mk_signings(negotiation, contributions): 31 | signings_dict = {} 32 | signings = [] 33 | for contribution in contributions: 34 | signings.append(models.Signing( 35 | negotiation=negotiation, 36 | signer_key_id=contribution.signer_key_id, 37 | signature=contribution.signature)) 38 | signings_dict[contribution.signer_key_id] = contribution.signature 39 | models.Signing.objects.bulk_create(signings) 40 | return signings_dict 41 | 42 | 43 | def get_latest_contributions(negotiation): 44 | return models.Contribution.objects.filter( 45 | negotiation=negotiation, latest=True) 46 | 47 | 48 | def check_close_negotiation(negotiation): 49 | latests = get_latest_contributions(negotiation) 50 | text_set = set(c.text for c in latests) 51 | if len(text_set) == 1: 52 | text = text_set.pop() 53 | if not utils.has_accept_meta(text): 54 | return 55 | 56 | now = get_now() 57 | signings_dict = mk_signings(negotiation, latests) 58 | hashable = { 59 | "timestamp": now.isoformat(), 60 | "negotiation_id": negotiation.id, 61 | "text": text, 62 | "signings": signings_dict, 63 | } 64 | consensus_id = utils.hash_with_canonical(hashable) 65 | negotiation.text = text 66 | negotiation.timestamp = now 67 | negotiation.consensus_id = consensus_id 68 | negotiation.status = models.NegotiationStatus.DONE 69 | negotiation.save() 70 | 71 | 72 | def contribute(request_data, negotiation_id, context): 73 | negotiation = get_negotiation_for_update(negotiation_id) 74 | if negotiation.status != models.NegotiationStatus.OPEN: 75 | raise errors.ValidationError("Negotiation is not open") 76 | text = request_data["text"] 77 | signature = request_data["signature"] 78 | signer_key_id = crypto_client.verify(signature, text) 79 | if signer_key_id is None: 80 | raise errors.ValidationError("Contribution's signature is not valid") 81 | 82 | negotiation.contributions.filter( 83 | signer_key_id=signer_key_id).update(latest=False) 84 | 85 | contrib = models.Contribution.objects.create( 86 | latest=True, 87 | negotiation=negotiation, text=text, 88 | signer_key_id=signer_key_id, signature=signature) 89 | 90 | check_close_negotiation(negotiation) 91 | return contrib 92 | -------------------------------------------------------------------------------- /consensus/consensus-service/consensus_service/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.15 on 2018-11-08 14:56 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Contribution', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('text', models.TextField()), 22 | ('latest', models.BooleanField()), 23 | ('signer_key_id', models.CharField(max_length=255)), 24 | ('signature', models.TextField()), 25 | ], 26 | ), 27 | migrations.CreateModel( 28 | name='Negotiation', 29 | fields=[ 30 | ('id', models.CharField(max_length=255, primary_key=True, serialize=False)), 31 | ('text', models.TextField(null=True)), 32 | ('status', models.CharField(choices=[('OPEN', 'OPEN'), ('DONE', 'DONE')], default='OPEN', max_length=255)), 33 | ('timestamp', models.DateTimeField(null=True)), 34 | ('consensus_id', models.CharField(max_length=255, null=True, unique=True)), 35 | ], 36 | ), 37 | migrations.CreateModel( 38 | name='Signing', 39 | fields=[ 40 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 41 | ('signer_key_id', models.CharField(max_length=255)), 42 | ('signature', models.TextField()), 43 | ('negotiation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='signings', to='consensus_service.Negotiation')), 44 | ], 45 | ), 46 | migrations.AddField( 47 | model_name='contribution', 48 | name='negotiation', 49 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contributions', to='consensus_service.Negotiation'), 50 | ), 51 | migrations.AlterIndexTogether( 52 | name='contribution', 53 | index_together=set([('negotiation', 'signer_key_id')]), 54 | ), 55 | ] 56 | -------------------------------------------------------------------------------- /consensus/consensus-service/consensus_service/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grnet/panoramix/a9b788e1a073e106d5542511094fe4ac3828d4b6/consensus/consensus-service/consensus_service/migrations/__init__.py -------------------------------------------------------------------------------- /consensus/consensus-service/consensus_service/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | from collections import namedtuple 4 | from django.db import models 5 | 6 | 7 | def to_choices(named): 8 | values = list(named) 9 | return [(value, value) for value in values] 10 | 11 | 12 | def mk_tuple(name, fields): 13 | tpl = namedtuple(name, fields) 14 | return tpl(*fields) 15 | 16 | 17 | NegotiationStatus = mk_tuple("NegotiationStatus", ["OPEN", "DONE"]) 18 | 19 | 20 | class Negotiation(models.Model): 21 | id = models.CharField(max_length=255, primary_key=True) 22 | text = models.TextField(null=True) 23 | status = models.CharField( 24 | max_length=255, choices=to_choices(NegotiationStatus), 25 | default=NegotiationStatus.OPEN) 26 | timestamp = models.DateTimeField(null=True) 27 | consensus_id = models.CharField(max_length=255, null=True, unique=True) 28 | 29 | 30 | finished_negotiations = models.Q(status=NegotiationStatus.DONE) 31 | 32 | 33 | class Signing(models.Model): 34 | negotiation = models.ForeignKey(Negotiation, related_name="signings") 35 | signer_key_id = models.CharField(max_length=255) 36 | signature = models.TextField() 37 | 38 | 39 | class Contribution(models.Model): 40 | negotiation = models.ForeignKey(Negotiation, related_name="contributions") 41 | text = models.TextField() 42 | latest = models.BooleanField() 43 | signer_key_id = models.CharField(max_length=255) 44 | signature = models.TextField() 45 | 46 | class Meta: 47 | index_together = ["negotiation", "signer_key_id"] 48 | -------------------------------------------------------------------------------- /consensus/consensus-service/consensus_service/rules.py: -------------------------------------------------------------------------------- 1 | # COLUMNS = ('collection', 'action', 'role', 'filter', 'check', 'fields', 'comment') 2 | 3 | RULES = [ 4 | ('consensus/negotiations', 'create', '*', '*', '*', '*', ''), 5 | ('consensus/negotiations', 'retrieve', '*', '*', '*', '*', ''), 6 | ('consensus/negotiations/contributions', 'create', '*', '*', '*', '*', ''), 7 | ('consensus/negotiations/contributions', 'retrieve', '*', '*', '*', '*', ''), 8 | ('consensus/consensus', 'list', '*', '*', '*', '*', ''), 9 | ('consensus/consensus', 'retrieve', '*', '*', '*', '*', ''), 10 | ] 11 | 12 | 13 | def get_rules(): 14 | return RULES 15 | -------------------------------------------------------------------------------- /consensus/consensus-service/consensus_service/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | from apimas_django.test import * 4 | from consensus_service import models 5 | from consensus_client import utils 6 | 7 | pytestmark = pytest.mark.django_db(transaction=False) 8 | 9 | 10 | def init_crypto(): 11 | pem = utils.make_ecdsa_signing_key() 12 | return utils.ECDSAClient(secret=pem) 13 | 14 | 15 | def test_disallowed(client): 16 | api = client.copy(prefix='/consensus/') 17 | 18 | r = api.get('negotiations') 19 | assert r.status_code == 405 20 | 21 | data = { 22 | 'contributions': [ 23 | { 24 | 'text': utils.prepare_text('a text', accept=True), 25 | 'signature': 'a long signature' 26 | } 27 | ] 28 | } 29 | 30 | r = api.post('negotiations', data) 31 | assert r.status_code == 400 32 | assert 'not writable' in r.content 33 | assert 'contributions' in r.content 34 | 35 | 36 | def test_negotiation_id(client): 37 | api = client.copy(prefix='/consensus/') 38 | r = api.post('negotiations', {'id': 'neg_id'}) 39 | assert r.status_code == 201 40 | body = r.json() 41 | assert body['status'] == 'OPEN' 42 | assert body['id'] == 'neg_id' 43 | 44 | r = api.post('negotiations', {'id': 'neg_id'}) 45 | assert r.status_code == 409 46 | 47 | 48 | def test_auto_close(client): 49 | api = client.copy(prefix='/consensus/') 50 | crypto = init_crypto() 51 | 52 | r = api.post('negotiations') 53 | assert r.status_code == 201 54 | body = r.json() 55 | assert body['status'] == 'OPEN' 56 | assert body['consensus_id'] is None 57 | neg_id = body['id'] 58 | 59 | neg_path = 'negotiations/%s' % neg_id 60 | text = utils.prepare_text('a text', accept=True) 61 | data = { 62 | 'text': text, 63 | 'signature': crypto.sign(text), 64 | } 65 | r = api.post('%s/contributions' % neg_path, data) 66 | assert r.status_code == 201 67 | body = r.json() 68 | assert body['latest'] is True 69 | 70 | r = api.get(neg_path) 71 | assert r.status_code == 200 72 | body = r.json() 73 | assert body['status'] == 'DONE' 74 | 75 | r = api.post('%s/contributions' % neg_path, data) 76 | assert r.status_code == 400 77 | assert 'Negotiation is not open' in r.content 78 | 79 | 80 | def test_two_users(client): 81 | api = client.copy(prefix='/consensus/') 82 | crypto1 = init_crypto() 83 | crypto2 = init_crypto() 84 | 85 | assert crypto1.key_id != crypto2.key_id 86 | 87 | # new negotiation 88 | r = api.post('negotiations') 89 | neg_id = r.json()['id'] 90 | neg_path = 'negotiations/%s' % neg_id 91 | 92 | text = utils.prepare_text('a text', accept=False) 93 | data = { 94 | 'text': text, 95 | 'signature': crypto1.sign(text), 96 | } 97 | r = api.post('%s/contributions' % neg_path, data) 98 | assert r.status_code == 201 99 | body = r.json() 100 | assert body['signer_key_id'] == crypto1.key_id 101 | signature11 = body['signature'] 102 | 103 | text = utils.prepare_text('a text', accept=True) 104 | data = { 105 | 'text': text, 106 | 'signature': crypto2.sign(text), 107 | } 108 | r = api.post('%s/contributions' % neg_path, data) 109 | assert r.status_code == 201 110 | body = r.json() 111 | assert body['signer_key_id'] == crypto2.key_id 112 | signature21 = body['signature'] 113 | 114 | r = api.get(neg_path) 115 | assert r.status_code == 200 116 | body = r.json() 117 | assert body['status'] == 'OPEN' 118 | assert body['consensus_id'] is None 119 | 120 | # Both users accept 121 | text = utils.prepare_text('a text', accept=True) 122 | data = { 123 | 'text': text, 124 | 'signature': crypto1.sign(text), 125 | } 126 | r = api.post('%s/contributions' % neg_path, data) 127 | assert r.status_code == 201 128 | body = r.json() 129 | contrib12 = body['id'] 130 | assert body['signer_key_id'] == crypto1.key_id 131 | signature12 = body['signature'] 132 | 133 | r = api.get(neg_path) 134 | assert r.status_code == 200 135 | body = r.json() 136 | assert body['status'] == 'DONE' 137 | assert len(body['contributions']) == 3 138 | consensus_id = body['consensus_id'] 139 | assert consensus_id is not None 140 | 141 | r = api.get('consensus/%s' % consensus_id) 142 | assert r.status_code == 200 143 | body = r.json() 144 | assert body['negotiation_id'] == neg_id 145 | assert body['text'] == utils.prepare_text('a text', accept=True) 146 | signings = body['signings'] 147 | assert len(signings) == 2 148 | assert any(sign['signature'] == signature21 for sign in signings) 149 | assert any(sign['signature'] == signature12 for sign in signings) 150 | -------------------------------------------------------------------------------- /consensus/consensus-service/consensus_service/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.shortcuts import render 5 | 6 | # Create your views here. 7 | -------------------------------------------------------------------------------- /consensus/consensus-service/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "consensus_django.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /consensus/consensus-service/requirements.txt: -------------------------------------------------------------------------------- 1 | apimas-django>=0.4a3,<0.5 2 | consensus-client 3 | -------------------------------------------------------------------------------- /consensus/consensus-service/requirements_dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | pytest==3.4.0 3 | pytest-cov==2.5.1 4 | pytest-django==3.1.2 5 | pytest-env==0.6.2 6 | pytest-profiling==1.2.11 7 | pytest-pythonpath==0.7.2 8 | pytest-watch==4.1.0 9 | -------------------------------------------------------------------------------- /consensus/consensus-service/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | CURPATH = os.path.dirname(os.path.realpath(__file__)) 5 | 6 | def get_requirements(): 7 | req_file = os.path.join(CURPATH, "requirements.txt") 8 | with open(req_file) as f: 9 | return [ 10 | x.strip('\n') 11 | for x in f.readlines() 12 | if x and x[0] != '#' 13 | ] 14 | 15 | version_file = os.path.join(CURPATH, "..", "..", "version.txt") 16 | with open(version_file) as f: 17 | version = f.read().strip() 18 | 19 | package_name = 'consensus-service' 20 | description = 'Panoramix negotiation and consensus service' 21 | 22 | setup( 23 | name=package_name, 24 | version=version, 25 | license='Affero GPL v3', 26 | author='GRNET S.A.', 27 | author_email='panoramix@dev.grnet.gr', 28 | description=description, 29 | packages=find_packages(), 30 | install_requires=get_requirements() 31 | ) 32 | -------------------------------------------------------------------------------- /consensus/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | python_files= 3 | **/tests.py 4 | DJANGO_SETTINGS_MODULE = consensus_django.test_settings 5 | -------------------------------------------------------------------------------- /consensus/run-functional-tests.sh: -------------------------------------------------------------------------------- 1 | if [ -z ${PYTHONPATH} ]; then 2 | echo Must set PYTHONPATH to the zeus location; 3 | exit; 4 | fi 5 | 6 | cd "$(dirname "$0")" 7 | TOP_DIR=$(pwd) 8 | 9 | for dir in $(ls tests) 10 | do 11 | cd tests/${dir} && ./run.sh && cd ${TOP_DIR} 12 | done 13 | -------------------------------------------------------------------------------- /consensus/run-unit-tests.sh: -------------------------------------------------------------------------------- 1 | pytest --nomigrations --pdb -s --maxfail=1 2 | -------------------------------------------------------------------------------- /consensus/tests/client/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | cd "$(dirname "$0")" 4 | 5 | WORKSPACE='/tmp/consensus_client_test' 6 | rm -rf ${WORKSPACE} 7 | mkdir -p ${WORKSPACE} 8 | 9 | export CONSENSUS_DATABASE=${WORKSPACE}/consensus_db.sqlite3 10 | 11 | python ../../consensus-service/manage.py migrate 12 | python ../../consensus-service/manage.py runserver 127.0.0.1:8000 --nothreading & 13 | sleep 3 14 | 15 | python test.py 16 | 17 | echo Killing daemons... 18 | pkill -f 'runserver 127.0.0.1:8000' 19 | -------------------------------------------------------------------------------- /consensus/tests/client/test.py: -------------------------------------------------------------------------------- 1 | from consensus_client.client import Client 2 | from consensus_client import utils 3 | 4 | pem = utils.make_ecdsa_signing_key() 5 | crypto = utils.ECDSAClient(pem) 6 | client = Client('http://127.0.0.1:8000/consensus/', crypto) 7 | 8 | body = client.negotiation_create() 9 | neg_id = body['id'] 10 | assert body['status'] == 'OPEN' 11 | print "Negotiation:" 12 | print body 13 | 14 | neg = client.negotiation_retrieve(neg_id) 15 | assert neg == body 16 | 17 | body = client.contribution_create(neg_id, 'TEXTTEXT', True) 18 | print "Contribution:" 19 | print body 20 | assert body['signer_key_id'] == crypto.key_id 21 | 22 | print "Verify:" 23 | assert client.contribution_verify(body) 24 | 25 | body = client.negotiation_retrieve(neg_id) 26 | print "Negotiation:" 27 | print body 28 | assert body['status'] == 'DONE' 29 | consensus_id = body['consensus_id'] 30 | 31 | body = client.consensus_retrieve(consensus_id) 32 | print "Consensus:" 33 | print body 34 | -------------------------------------------------------------------------------- /consensus/tests/events/config/admin: -------------------------------------------------------------------------------- 1 | { 2 | "ROLE": "ADMIN", 3 | "SECRET_KEY": "-----BEGIN EC PRIVATE KEY-----\nMF8CAQEEGIO+/E6oXKwPCzFljzFDKiOqtrtGVenXZ6AKBggqhkjOPQMBAaE0AzIA\nBBf29XfL0IaMAecdKDFYJ2sSW79rDMtTQ9ktdstiKN950barugBf9T8IU18i0cPT\nqA==\n-----END EC PRIVATE KEY-----\n", 4 | "ENDPOINT": "http://127.0.0.1:8000/consensus/" 5 | } -------------------------------------------------------------------------------- /consensus/tests/events/config/trustee: -------------------------------------------------------------------------------- 1 | { 2 | "ROLE": "TRUSTEE", 3 | "SECRET_KEY": "-----BEGIN EC PRIVATE KEY-----\nMF8CAQEEGAaOgWYcs6BaR/FqU0fq28dBls/tmmDOVKAKBggqhkjOPQMBAaE0AzIA\nBHYmaqN21vf4QWBamzYW4ENWmabJV6oDdQr8kIABK3shKx26/7tSPipsjWn4G754\nTA==\n-----END EC PRIVATE KEY-----\n", 4 | "ENDPOINT": "http://127.0.0.1:8000/consensus/" 5 | } -------------------------------------------------------------------------------- /consensus/tests/events/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | cd "$(dirname "$0")" 4 | 5 | export WORKSPACE='/tmp/consensus_events_test' 6 | rm -rf ${WORKSPACE} 7 | mkdir -p ${WORKSPACE} 8 | cp -r config/* ${WORKSPACE} 9 | 10 | export CONSENSUS_DATABASE=${WORKSPACE}/consensus_db.sqlite3 11 | 12 | python ../../consensus-service/manage.py migrate 13 | python ../../consensus-service/manage.py runserver 127.0.0.1:8000 --nothreading & 14 | sleep 3 15 | 16 | python test.py admin & 17 | python test.py trustee & 18 | 19 | python test.py check_done 20 | 21 | echo Killing daemons... 22 | pkill -f 'runserver 127.0.0.1:8000' 23 | -------------------------------------------------------------------------------- /consensus/tests/events/test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | from consensus_client import zeus_events 4 | from consensus_client.config import Config 5 | import sys 6 | WORKSPACE = os.environ['WORKSPACE'] 7 | 8 | role = sys.argv[1] # 'admin' or 'trustee' or 'check_done' 9 | 10 | if role == 'check_done': 11 | admin_cfg_file = os.path.join(WORKSPACE, 'admin') 12 | admin_cfg = Config(admin_cfg_file) 13 | while True: 14 | decryption = admin_cfg.get_value('decryption') 15 | if decryption is not None: 16 | break 17 | admin_cfg.reload() 18 | time.sleep(2) 19 | 20 | else: 21 | cfg_file = os.path.join(WORKSPACE, role) 22 | pad_file = os.path.join(WORKSPACE, 'pad') 23 | doc_file = os.path.join(WORKSPACE, 'election.zeus') 24 | 25 | zeus_events.main(cfg_file, pad_file, doc_file) 26 | -------------------------------------------------------------------------------- /demo/README: -------------------------------------------------------------------------------- 1 | Panoramix Demo 2 | ============== 3 | 4 | This directory contains a demo for each supported backend: 'gpg', 'zeus', 5 | and 'sphinxmix'. 6 | 7 | First initialize the mixnet database with 8 | 9 | ./demo/init.sh 10 | 11 | and then start the server for the preferred backend in a terminal with: 12 | 13 | ./demo/launch-.sh 14 | 15 | In another terminal, run the client-side demo; for 'gpg' or 'sphinxmix': 16 | 17 | ./demo/demo-.sh 18 | 19 | For 'zeus', an extended interactive evoting demo is included that involves 20 | two users: 'peer1' and 'peer2'. In two separate terminals, run: 21 | 22 | ./demo/demo-zeus.sh 23 | -------------------------------------------------------------------------------- /demo/conf/ec1_settings: -------------------------------------------------------------------------------- 1 | { 2 | "CATALOG_URL": { 3 | "description": null, 4 | "value": "http://127.0.0.1:8000/", 5 | "title": "CATALOG_URL" 6 | }, 7 | "CRYPTO_BACKEND": { 8 | "description": null, 9 | "value": "panoramix.backends.sphinxmix_backend", 10 | "title": "CRYPTO_BACKEND" 11 | }, 12 | "CRYPTO_PARAMS": { 13 | "description": null, 14 | "value": { 15 | "BODY_LEN": 1024, 16 | "GROUP": 713, 17 | "HEADER_LEN": 192 18 | }, 19 | "title": "CRYPTO_PARAMS" 20 | }, 21 | "KEY": { 22 | "description": null, 23 | "value": { 24 | "SECRET": "5cbe4e3da463a3812b9e97ff7b72f652bd26d6e935254891594b2497", 25 | "PUBLIC": "030e523aae1865457a73174ecc94d91df47440202984f0fd471e98f1a6" 26 | }, 27 | "title": "KEY" 28 | } 29 | } -------------------------------------------------------------------------------- /demo/conf/ec2_settings: -------------------------------------------------------------------------------- 1 | { 2 | "CATALOG_URL": { 3 | "description": null, 4 | "value": "http://127.0.0.1:8000/", 5 | "title": "CATALOG_URL" 6 | }, 7 | "CRYPTO_BACKEND": { 8 | "description": null, 9 | "value": "panoramix.backends.sphinxmix_backend", 10 | "title": "CRYPTO_BACKEND" 11 | }, 12 | "CRYPTO_PARAMS": { 13 | "description": null, 14 | "value": { 15 | "BODY_LEN": 1024, 16 | "GROUP": 713, 17 | "HEADER_LEN": 192 18 | }, 19 | "title": "CRYPTO_PARAMS" 20 | }, 21 | "KEY": { 22 | "description": null, 23 | "value": { 24 | "SECRET": "254864e686876df9b4d075d7c8d5885577855e76ff665a9437328e20", 25 | "PUBLIC": "0371a23d7363f13a89afa364b6e8ef40083f3d4b959b8fb97fc6d7c86f" 26 | }, 27 | "title": "KEY" 28 | } 29 | } -------------------------------------------------------------------------------- /demo/conf/gpg_server.conf: -------------------------------------------------------------------------------- 1 | { 2 | "CRYPTO_BACKEND": { 3 | "description": null, 4 | "value": "panoramix.backends.gpg_backend", 5 | "title": "CRYPTO_BACKEND" 6 | }, 7 | "GPG_HOMEDIR": { 8 | "description": null, 9 | "value": "/tmp/mixnet_gnupg", 10 | "title": "GPG_HOMEDIR" 11 | } 12 | } -------------------------------------------------------------------------------- /demo/conf/key1: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PRIVATE KEY BLOCK----- 2 | Version: GnuPG v1 3 | 4 | lQH+BFa53fQBBADGhdyNIYjggwcd3w60oWaEljhxxvS41BA22ldp30eHkyfKBTvz 5 | A8KCQPxLuXcWMP0x3BcWLEg1g2gJiXC+9s/JidZKDuHzdS9vDhaEl+rP5LBN98sR 6 | 8M+FpLJF1og/59JEma3iHFYnmzJ8uQkPTmdgQLjQ40+0cOguTnNyVDQ6lwARAQAB 7 | /gMDAr7CmMXvGxBSYLmghsus+z/bjW0uvVbJNUsE6RwyIDqjNmQl3LELZPM7eu2d 8 | Q1RpyEhi30OYcDkjKAPyQUpVnpD63pfUujfGq/TLbIoueFJ3gpfhf79vumOqehyp 9 | 3aRmnsXKsJdLH5vh3WvY87LowL0wQ1tNXaVWsrHtQnRgU/6U/G8lPVFJ7ZGAyzGB 10 | B5dgEI16fRdKQLESx75xnTEQvEQs7mlel4VZ6B4VJL/9OC5RrP1hapbvdNruCm1X 11 | xRl3G+UJ0X1d8lwOZ05NqZSPoUhgcSc7aW3wygqN2A5b+pr7I4eMVHcEwyGipZcq 12 | VFtF0YIQmYdtE8DJ1mcXsazalePDmVwJ+V0lSEEXUR6mP/nvLFg8JbMbagezf0Hz 13 | oyGyfITggFOOCV/DYB23pHNdV6DC1GbHOGaD+R9QbMsn6TelV1Wq9UxR/kykn6CU 14 | 9Za+IsDwg0hgQl/g6MDMSoLfS9fYHh7e4n167obvHq+ltBlUZXN0MyA8dGVzdDNA 15 | ZXhhbXBsZS5jb20+iLgEEwECACIFAla53fQCGwMGCwkIBwMCBhUIAgkKCwQWAgMB 16 | Ah4BAheAAAoJEBPBgzWgKb7F2o4EALzFaOvnUih/kEI5ntwOs2p1lhwxx2vSqhkw 17 | l/sNeDpW6Ol+ejfbNSxhAj6c+BfSnF28DXA7/QM6JTKWtvXoMTb2IyC8aVntPjYl 18 | QO/IwCFpRJqTgYBJQwSBmnpMlUljEZhC/MBOgzWmYQIuc0g3UAmsBS/6gCczr7jV 19 | P33GLFLanQH+BFa53fQBBADfio2YZGDPimw81/MAn6JS4Smr2Xa5nUTC3WlHXfK/ 20 | WBw0CR5jRXG1O9A6VVSE9YhlgKZPIBReujTy+PV4Gggy3IKPO/MDqlqSyXrhgr2f 21 | c4jasiUXKhM8CDJ4SlxgzDQfxCLqBDxQ3FZhTlKp5ytLFmA4dNrqPmPfPPrnzXed 22 | cQARAQAB/gMDAr7CmMXvGxBSYA7OuX5RXE85u17IHC16ZrnS1RXVbq/abZxzuOTT 23 | 8VntgO07WDdRy8S9CzbyieZ43tqJQUnQT3NRcHt84c85N9P3114Jc9VlcfP/p+2k 24 | nZEUPr4ROj4qGjgzEXGbvbOx+TbbecK136dhZa8a57aBsCN1NcGWbmFJJpTx+ba6 25 | jv4js/DLzFoSG24O3SefRymV8n6SLeaCRYrMpW3f5LpKf1Tw0Eav6G5lpqEOGi1C 26 | e3RhpyBdcrDttO34lTdwlBOtJ9VIui0gWdgmTgLGiNM0YwSpRQn3jP0M+iEx/VyT 27 | RnHzA9YiPgVK0HFg3VzrdqZx/fW9qrqVX1cBqQLO7tguXCXwK6QB974ve1tQ1aUZ 28 | zngZpscZCL95OXhP+f4JMInGEbmjTSFunBDxo1+Af6z6xHWB0XSBEk0ysU49/nU4 29 | TlpJNHjI2PsMvwdv48PmoJY4QzSJhzbhqeCvwgwfDwPK6WAJtfIuiJ8EGAECAAkF 30 | Ala53fQCGwwACgkQE8GDNaApvsU5SwP/XPiRR/lg8b2vFEhHJscLO5tr6rQSLIfc 31 | Q24fHlpL4cl0HxZSrEL/XBeIVXoR4VY/jTjs0NQAq813KfBwE3FaMiuL8CbRig11 32 | vPa8eq8v1jQN3bXiUyQNAlztUfURKeqfldX2pbuXTnMSMIyOtjp+Xf/PXb4dGb2f 33 | JOBAGaR9MOU= 34 | =Z3zA 35 | -----END PGP PRIVATE KEY BLOCK----- 36 | -------------------------------------------------------------------------------- /demo/conf/key2: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PRIVATE KEY BLOCK----- 2 | Version: GnuPG v1 3 | 4 | lQH+BFa5tIkBBACpkV/BEDBothM3XZSDfyW07Oxu1qSWwmmzN8erC+ysoOqZ7CTp 5 | UB/ImsavJjsX8zO2di7Hdkale1G5AWaQw7SGFbb4UwiUmmn/yiHDV1zFr46utH2X 6 | ShLO3gXQceIc2t+fAbiNQJKgm09k6sOsRVjqj32RRTB4LNqCO22nbaJiFQARAQAB 7 | /gMDAgvtxUozOJebYDuL1TDETR8gL28/Hr5H4/03HFTURbkPdnjX4M3WVm2hNtse 8 | KfvcGPcy+GgS3g7gH0bJ6cxIAUnzkhx4XXaJcZHnJmQIu6TRZEAtt7Enc4oQz3St 9 | dfWR2JAOrib3Ljmmndv0G4sAei3E98K4N9dnf0RXAq5J9aCFLLsM/Zabr1ea6LDo 10 | /g12741vERGOZZLsOw5adeAqhjPrtzyWgDm6X10bsl1ICfOihqDzIX5r+7+qaPiY 11 | YcP2gMJUYDzVYGxb3oinYcoOk4/wjzbe8QNBwZkKzfBquhX84rwx2hWlNtcG4Lem 12 | 8Uf72AC7/aTdmv109pikRV+CR04EzrdjV3dEbc8M3R/FjxQXTJrKfo7a1YjNr/xI 13 | rNnZO/QbHJIQ80uVTT7ZPNResP7e83y915Ta+7EDj3b2A4wf/BvMNZ2QmsbcClAs 14 | FufIxbyJoxat6xuhS+X+nb/aEK7EEQTIFY3v2q4ZyBuitBlUZXN0MiA8dGVzdDJA 15 | ZXhhbXBsZS5jb20+iLgEEwECACIFAla5tIkCGwMGCwkIBwMCBhUIAgkKCwQWAgMB 16 | Ah4BAheAAAoJEPxlDKD3dJ/wBS8EAIcoMVlHP7dlYC0w3X7xW8SORAhwV963KZ4K 17 | 1eC45T1CAZvCcNZYlaGyVM6jgiC70c6aDW5ckPzvfT9qjtTm1Cgg16RJflveYGgt 18 | IC9k1JUoOZ82rsnfqMLpe+IxXL8iK3EZpWCWPHLiaS9DJ8EIRKEDgi+kOmpbQVdg 19 | atAgRvv+nQH+BFa5tIkBBADOMaI7mRJ/DX/HCjuvOMniNbykbA7PNbbwFSvouai2 20 | PAQnYJxmwLobL60nsIftXatqToRAl4QfdK72NEfCczysOUiB2nrCCiDmRu0GAl75 21 | G2eOGw1s9UdLSdrBqKIF26lE7QBuHq/g4wbrlajwk5EPsCzo6yathJZJJN2DwZqx 22 | KQARAQAB/gMDAgvtxUozOJebYPCntpnFtCfO3Rv/uMAEzdBV+4gx0inTIFymm4R8 23 | k5cbX8NrRK8Kw4aVLxrnq6seSQ4s9npZ4j8gBYLrMlUbnmFXWsCzLCBDasM91NYN 24 | KyA5iy9rHmkzjzevQrB3HIWWcDBoZCE24NDMvb9GCMxHZimv26EUGylnRhlM+IUL 25 | n9HFwtXXfdLdZQkHHJHFpEUBwKqvw3hl0bMxfVAv16YZI3nzuclFzUzTJgoWZxnL 26 | RYyx9Pk/Z+ahxSZ/FolA0I1VEuiY9JFiOgqqIvRRpBOtNXc1F7byPAapQtqgRSCb 27 | HK42+ig769c2oDqs09DudvpdBh/BVt1RyBAmS3oDncngdPiBe/tKUTOUKY/TnGwg 28 | s5a9n017l0RZAuIlg6AX9isgot+3dD58didXKAnr7PCHZKYyqKZEuanp1CqvMR1+ 29 | 3a8k9z4GGQENCDTwkg4bHg3gRwlJ2Wy1/UgEwL5yj5+r3aNSw4gSiJ8EGAECAAkF 30 | Ala5tIkCGwwACgkQ/GUMoPd0n/Au7AQAiqoMeMRh0rNVXoFgc7WwfoHCk5MX+8Mj 31 | 0Zsa730cfKSVrmPfe5cO3NEH5PxELXGgsmOeJ0CGrZ3iyZZLfufAjyM9o02KoxR4 32 | L+J7PrwSZi254DG+TXA7SlYrTyRjFUTSGrW3IECI1MZF5RUKytd9I8g/EBuOZE4n 33 | oK2CR+0tB9Y= 34 | =Ai9h 35 | -----END PGP PRIVATE KEY BLOCK----- 36 | -------------------------------------------------------------------------------- /demo/conf/sphinxmix_server.conf: -------------------------------------------------------------------------------- 1 | { 2 | "CRYPTO_BACKEND": { 3 | "description": null, 4 | "value": "panoramix.backends.sphinxmix_backend", 5 | "title": "CRYPTO_BACKEND" 6 | }, 7 | "CRYPTO_PARAMS": { 8 | "description": null, 9 | "value": { 10 | "BODY_LEN": 1024, 11 | "GROUP": 713, 12 | "HEADER_LEN": 192 13 | }, 14 | "title": "CRYPTO_PARAMS" 15 | } 16 | } -------------------------------------------------------------------------------- /demo/conf/user1_settings: -------------------------------------------------------------------------------- 1 | { 2 | "CATALOG_URL": { 3 | "description": null, 4 | "value": "http://127.0.0.1:8000/", 5 | "title": "CATALOG_URL" 6 | }, 7 | "GPG_HOMEDIR": { 8 | "description": null, 9 | "value": "/tmp/gpgs/1", 10 | "title": "GPG_HOMEDIR" 11 | }, 12 | "CRYPTO_BACKEND": { 13 | "description": null, 14 | "value": "panoramix.backends.gpg_backend", 15 | "title": "CRYPTO_BACKEND" 16 | }, 17 | "KEY": { 18 | "description": null, 19 | "value": { 20 | "GPG_PASSPHRASE": "test3", 21 | "GPG_KEYID": "13C18335A029BEC5" 22 | }, 23 | "title": "KEY" 24 | } 25 | } -------------------------------------------------------------------------------- /demo/conf/user2_settings: -------------------------------------------------------------------------------- 1 | { 2 | "CATALOG_URL": { 3 | "description": null, 4 | "value": "http://127.0.0.1:8000/", 5 | "title": "CATALOG_URL" 6 | }, 7 | "GPG_HOMEDIR": { 8 | "description": null, 9 | "value": "/tmp/gpgs/2", 10 | "title": "GPG_HOMEDIR" 11 | }, 12 | "CRYPTO_BACKEND": { 13 | "description": null, 14 | "value": "panoramix.backends.gpg_backend", 15 | "title": "CRYPTO_BACKEND" 16 | }, 17 | "KEY": { 18 | "description": null, 19 | "value": { 20 | "GPG_PASSPHRASE": "test2", 21 | "GPG_KEYID": "FC650CA0F7749FF0" 22 | }, 23 | "title": "KEY" 24 | } 25 | } -------------------------------------------------------------------------------- /demo/conf/zeus1: -------------------------------------------------------------------------------- 1 | {"SECRET": 12345, 2 | "CRYPTO_BACKEND": "ZEUS", 3 | "PUBLIC": 15440008421441127168372068809074609019832402685910570147294994852616715955978528105192237906991852285899107605111337913237414888980546601825010796966196310359430668988420197678991517680475693016693971052029882891607501380835676198592037489273810510539915102450447423960704265339831088323684194054889216028771257963581716820130053432142190929106824191067330331604477776642698470966817034346828089989654854863644811476530334633714262718527798725287617825792174661552556392306996425048736590658574986427328408283118171205025215001510304391404096280919428358980113527124722299136497795876928637431574069145485213383815033 4 | } 5 | -------------------------------------------------------------------------------- /demo/conf/zeus1_keyid: -------------------------------------------------------------------------------- 1 | 292918e814abd7ab9e476d99bf861f016cc4ffec765dabc010fc33aab0721dfd -------------------------------------------------------------------------------- /demo/conf/zeus2: -------------------------------------------------------------------------------- 1 | {"SECRET": 23456, 2 | "CRYPTO_BACKEND": "ZEUS", 3 | "PUBLIC": 10983433001431982921630013285956744705943214689726085246838250540222232819442767349114478360032421121049780073721515458605254502009455345589320404163296752265628622976049186780446785139119859339090299004849509335355727612190551233613033060390233917439972447357030869461968915047933962039205875293812912368255098409203079598235367169085889226512605229384002238215800082970349862792749155291249567245165711713974991998547470144658381026274182937625592669670548708314230210737057880176411032899092836064767595544836500251096575019833269973193770463569818491375941561641914177094214795788050846428519317092667860735221842 4 | } 5 | -------------------------------------------------------------------------------- /demo/conf/zeus2_keyid: -------------------------------------------------------------------------------- 1 | c303bc69c06008161352be15dad4ff06d3eea21dd4aeac6633387c8c91875aaf -------------------------------------------------------------------------------- /demo/conf/zeus_joined: -------------------------------------------------------------------------------- 1 | {"key_data": "5816a9d480554dbfdacbfd294d85dec3a33c81a050b62d5f7666dc58a066b21265ded74e6606afbdcac4d360327aaf0143e9051a2f93619a921169e6306fa2fcbac90149e7b62d3b442e0f17d534df5be42fe95a21add2929a413cc71954b634653bf2ec12ea818df4ac1906926bf700cf5c5fc133c7b0b91ef77b71cc6d1cc0e3e7c69e9f1a9bb544cb65c6e37e3fc34857bb2d4fd5b28b6d3c3747c4f77c7e2a593493562ebaa0e928424caead44a83bb3bcfa4a80410eac6679bef72ad1b898577ad58793722b131bc04e6f6c69d185ae4d0b105348823517b69a7bb12ab57d3a599e2ff2f0c2cf095e81a188c626cb3ada10c6cf15086c7ce2b6b92df4ec"} 2 | -------------------------------------------------------------------------------- /demo/conf/zeus_joined_keyid: -------------------------------------------------------------------------------- 1 | 2b4894d505683414f9e8a10d214b47a3bcea1875896850d48cba17763916f6a8 -------------------------------------------------------------------------------- /demo/conf/zeus_server.conf: -------------------------------------------------------------------------------- 1 | {"CRYPTO_BACKEND": "panoramix.backends.zeus_backend"} 2 | -------------------------------------------------------------------------------- /demo/demo-gpg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # run with --debug for traceback 4 | 5 | cd "$(dirname "$0")" 6 | CURDIR=$(pwd) 7 | CONF=$CURDIR/conf 8 | 9 | ARGS=${*} 10 | 11 | source cli_functions.bash 12 | source mixnet_functions.bash 13 | 14 | GPGHOMEDIR1=/tmp/gpgs/1 15 | GPGHOMEDIR2=/tmp/gpgs/2 16 | 17 | rm -rf /tmp/gpgs 18 | 19 | mkdir -p $GPGHOMEDIR1 20 | chmod 700 $GPGHOMEDIR1 21 | mkdir -p $GPGHOMEDIR2 22 | chmod 700 $GPGHOMEDIR2 23 | 24 | gpg --homedir $GPGHOMEDIR1 --import $CONF/key1 25 | gpg --homedir $GPGHOMEDIR2 --import $CONF/key2 26 | 27 | exe export PANORAMIX_CONFIG=$CONF/user1_settings 28 | 29 | 30 | echo 31 | exe $CMD peer list 32 | 33 | echo 34 | exe $CMD key show 35 | 36 | echo 37 | read -p "Create peer: " Y 38 | 39 | with_self_consensus "peer create --name peer1" 40 | 41 | exe $CMD peer info --peer-id 13C18335A029BEC5 42 | 43 | ENDPOINT1="ONION_PEER1" 44 | 45 | echo 46 | read -p "Create endpoint $ENDPOINT1: " Y 47 | 48 | with_self_consensus "endpoint create --endpoint-id ${ENDPOINT1} --peer-id 13C18335A029BEC5 --endpoint-type ONION --size-min 3 --size-max 10" 49 | EP_CREATE_CONSENSUS="${consensus}" 50 | 51 | echo 52 | read -p "List endpoints: " Y 53 | 54 | echo 55 | exe $CMD endpoint list --peer-id 13C18335A029BEC5 56 | 57 | echo 58 | exe export PANORAMIX_CONFIG=$CONF/user2_settings 59 | 60 | echo 61 | read -p "Create second peer: " Y 62 | 63 | with_self_consensus "peer create --name peer2" 64 | 65 | echo 66 | read -p "Import peer data: " Y 67 | 68 | echo 69 | exe $CMD peer import --peer-id 13C18335A029BEC5 70 | 71 | echo 72 | read -p "Send first message: " Y 73 | 74 | echo 75 | exe $CMD message send --recipient 13C18335A029BEC5 --endpoint-id ${ENDPOINT1} --data "first text" 76 | 77 | echo 78 | exe $CMD inbox list --endpoint-id ${ENDPOINT1} 79 | 80 | echo 81 | read -p "Send two more messages: " Y 82 | 83 | echo 84 | exe $CMD message send --recipient 13C18335A029BEC5 --endpoint-id ${ENDPOINT1} --data "second text" 85 | 86 | echo 87 | exe $CMD message send --recipient 13C18335A029BEC5 --endpoint-id ${ENDPOINT1} --data "third text" 88 | 89 | echo 90 | read -p "List inbox again: " Y 91 | 92 | echo 93 | exe $CMD inbox list --endpoint-id ${ENDPOINT1} 94 | 95 | echo 96 | exe export PANORAMIX_CONFIG=$CONF/user1_settings 97 | 98 | echo 99 | read -p "Endpoint owner closes the inbox: " Y 100 | 101 | ACCEPTED_LOG=/tmp/INBOX_ACCEPTED_LOG_GPG_PEER1 102 | write_hashes_log ${ENDPOINT1} ${ACCEPTED_LOG} 103 | with_self_consensus "inbox close --endpoint-id ${ENDPOINT1} --on-last-consensus-id ${EP_CREATE_CONSENSUS} --from-log ${ACCEPTED_LOG}" 104 | EP_CLOSE_CONSENSUS="${consensus}" 105 | 106 | echo 107 | exe $CMD endpoint info --endpoint-id "${ENDPOINT1}" 108 | 109 | echo 110 | read -p "Endpoint owner processes the inbox: " Y 111 | 112 | echo 113 | PROCESS_LOG=/tmp/INBOX_PROCESS_LOG_GPG_PEER1 114 | exe $CMD inbox process --peer-id 13C18335A029BEC5 --endpoint-id ${ENDPOINT1} --process-log-file "${PROCESS_LOG}" --upload 115 | 116 | echo 117 | read -p "Endpoint owner acknowledges the outbox: " Y 118 | 119 | with_self_consensus "processed ack --endpoint-id ${ENDPOINT1} --on-last-consensus-id ${EP_CLOSE_CONSENSUS} --from-log ${PROCESS_LOG}" 120 | 121 | echo 122 | read -p "Check endpoint status: " Y 123 | 124 | echo 125 | exe $CMD endpoint info --endpoint-id "${ENDPOINT1}" 126 | 127 | echo 128 | read -p "List outbox: " Y 129 | 130 | echo 131 | exe $CMD outbox list --endpoint-id "${ENDPOINT1}" 132 | -------------------------------------------------------------------------------- /demo/demo-sphinxmix.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # run with --debug for traceback 4 | 5 | cd "$(dirname "$0")" 6 | CURDIR=$(pwd) 7 | CONF=$CURDIR/conf 8 | 9 | ARGS=${*} 10 | 11 | source cli_functions.bash 12 | source mixnet_functions.bash 13 | 14 | exe export PANORAMIX_CONFIG=$CONF/ec1_settings 15 | 16 | 17 | echo 18 | exe $CMD peer list 19 | 20 | echo 21 | exe $CMD key show 22 | 23 | echo 24 | read -p "Create peer: " Y 25 | 26 | with_self_consensus "peer create --name peer1" 27 | PEER1_ID="${consensus_result}" 28 | 29 | exe $CMD peer info --peer-id ${PEER1_ID} 30 | 31 | ENDPOINT1="SPHINX_PEER1" 32 | 33 | echo 34 | read -p "Create endpoint $ENDPOINT1: " Y 35 | 36 | with_self_consensus "endpoint create --endpoint-id ${ENDPOINT1} --peer-id ${PEER1_ID} --endpoint-type SPHINXMIX --size-min 3 --size-max 10" 37 | EP_CREATE_CONSENSUS="${consensus}" 38 | 39 | echo 40 | read -p "List endpoints: " Y 41 | 42 | echo 43 | exe $CMD endpoint list --peer-id ${PEER1_ID} 44 | 45 | echo 46 | exe export PANORAMIX_CONFIG=$CONF/ec2_settings 47 | 48 | echo 49 | read -p "Create second peer: " Y 50 | 51 | with_self_consensus "peer create --name peer2" 52 | PEER2_ID="${consensus_result}" 53 | 54 | echo 55 | read -p "Import peer data: " Y 56 | 57 | echo 58 | exe $CMD peer import --peer-id ${PEER1_ID} 59 | 60 | echo 61 | read -p "Send first message: " Y 62 | 63 | echo 64 | exe $CMD message send --recipients ${PEER1_ID},${PEER2_ID},${PEER2_ID} --endpoint-id ${ENDPOINT1} --data "first text" 65 | 66 | echo 67 | exe $CMD inbox list --endpoint-id ${ENDPOINT1} 68 | 69 | echo 70 | read -p "Send two more messages: " Y 71 | 72 | echo 73 | exe $CMD message send --recipients ${PEER1_ID},${PEER2_ID} --endpoint-id ${ENDPOINT1} --data "second text" 74 | 75 | echo 76 | exe $CMD message send --recipients ${PEER1_ID},${PEER2_ID} --endpoint-id ${ENDPOINT1} --data "third text" 77 | 78 | echo 79 | read -p "List inbox again: " Y 80 | 81 | echo 82 | exe $CMD inbox list --endpoint-id ${ENDPOINT1} 83 | 84 | echo 85 | exe export PANORAMIX_CONFIG=$CONF/ec1_settings 86 | 87 | echo 88 | read -p "Endpoint owner closes the inbox: " Y 89 | 90 | ACCEPTED_LOG=/tmp/INBOX_ACCEPTED_LOG_GPG_PEER1 91 | write_hashes_log ${ENDPOINT1} ${ACCEPTED_LOG} 92 | with_self_consensus "inbox close --endpoint-id ${ENDPOINT1} --on-last-consensus-id ${EP_CREATE_CONSENSUS} --from-log ${ACCEPTED_LOG}" 93 | EP_CLOSE_CONSENSUS="${consensus}" 94 | 95 | echo 96 | exe $CMD endpoint info --endpoint-id "${ENDPOINT1}" 97 | 98 | echo 99 | read -p "Endpoint owner processes the inbox: " Y 100 | 101 | echo 102 | PROCESS_LOG=/tmp/INBOX_PROCESS_LOG_GPG_PEER1 103 | exe $CMD inbox process --peer-id ${PEER1_ID} --endpoint-id ${ENDPOINT1} --process-log-file "${PROCESS_LOG}" --upload 104 | 105 | echo 106 | read -p "Endpoint owner acknowledges the outbox: " Y 107 | 108 | with_self_consensus "processed ack --endpoint-id ${ENDPOINT1} --on-last-consensus-id ${EP_CLOSE_CONSENSUS} --from-log ${PROCESS_LOG}" 109 | 110 | echo 111 | read -p "Check endpoint status: " Y 112 | 113 | echo 114 | exe $CMD endpoint info --endpoint-id "${ENDPOINT1}" 115 | 116 | echo 117 | read -p "List outbox: " Y 118 | 119 | echo 120 | exe $CMD outbox list --endpoint-id "${ENDPOINT1}" 121 | -------------------------------------------------------------------------------- /demo/init.sh: -------------------------------------------------------------------------------- 1 | if [ -z "${TMPDIR}" ]; then 2 | TMPDIR=/tmp 3 | fi 4 | 5 | SHARE_CHANNEL="${TMPDIR}/demo_share_channel" 6 | rm -rf "${SHARE_CHANNEL}" 7 | 8 | rm -f /tmp/panoramix.db.sqlite3; panoramix-manage migrate; rm -rf /tmp/mixnet_gnupg 9 | -------------------------------------------------------------------------------- /demo/launch-gpg.sh: -------------------------------------------------------------------------------- 1 | cd "$(dirname "$0")" 2 | 3 | export PANORAMIX_CONFIG=$(pwd)/conf/gpg_server.conf 4 | panoramix-manage runserver $1 5 | -------------------------------------------------------------------------------- /demo/launch-sphinxmix.sh: -------------------------------------------------------------------------------- 1 | cd "$(dirname "$0")" 2 | 3 | export PANORAMIX_CONFIG=$(pwd)/conf/sphinxmix_server.conf 4 | panoramix-manage runserver $1 5 | -------------------------------------------------------------------------------- /demo/launch-zeus.sh: -------------------------------------------------------------------------------- 1 | cd "$(dirname "$0")" 2 | 3 | export PANORAMIX_CONFIG=$(pwd)/conf/zeus_server.conf 4 | panoramix-manage runserver $1 5 | -------------------------------------------------------------------------------- /demo/mixnet_functions.bash: -------------------------------------------------------------------------------- 1 | source cli_functions.bash 2 | 3 | CMD="panoramix $ARGS" 4 | 5 | prepare_neg () { 6 | local varname="$1" 7 | local filename="${SHARE_CHANNEL}/negotiations/${varname}" 8 | if [ ! -f "${filename}" ]; then 9 | local tmp_negid=$(exe $CMD negotiation create) 10 | echo creating ${tmp_negid} 11 | echo "${tmp_negid}" >> "${filename}" 12 | fi 13 | export ${varname}="$(head -n 1 "${filename}")" 14 | } 15 | 16 | contrib_loop () { 17 | local negid="$1" 18 | local contrib= 19 | while true; do 20 | export consensus="$(exe ${CMD} negotiation info --negotiation-id=${negid} -f value -c consensus)" 21 | echo consensus "$consensus" 22 | if [ "${consensus}" != "None" ]; then 23 | break 24 | fi 25 | echo 26 | exe ${CMD} contribution list --negotiation-id="${negid}" 27 | read -p "Enter contribution id to accept (or 'contrib' to make own contribution, or to review list): " contrib 28 | if [ "${contrib}" = "contrib" ]; then 29 | read -p "Command: " COMMAND; echo; exe $CMD $COMMAND; 30 | elif [ -n "${contrib}" ]; then 31 | echo; exe $CMD contribution accept --negotiation-id=${negid} --contribution-id=${contrib} 32 | fi 33 | done 34 | } 35 | 36 | run_if_not_file () { 37 | COMMAND="$1" 38 | FILENAME="$2" 39 | while true; do 40 | OUTPUT="$(exe ${CMD} ${COMMAND})" 41 | if [ ! -f "${FILENAME}" ]; then 42 | sleep 0.5 43 | else 44 | echo $OUTPUT 45 | break 46 | fi 47 | done 48 | } 49 | 50 | with_self_consensus () { 51 | COMMAND="$1" 52 | SELF_NEG="$(exe $CMD negotiation create)" 53 | exe ${CMD} ${COMMAND} --negotiation-id="${SELF_NEG}" --accept 54 | contrib_loop "${SELF_NEG}" 55 | OUTPUT="$(exe ${CMD} ${COMMAND} --consensus-id ${consensus})" 56 | echo $OUTPUT 57 | export consensus_result="${OUTPUT}" 58 | } 59 | 60 | run_with_neg () { 61 | NEG_NAME="$1" 62 | COMMAND="$2" 63 | prepare_neg "${NEG_NAME}" 64 | eval negid="\$${NEG_NAME}" 65 | exe ${CMD} ${COMMAND} --negotiation-id="${negid}" 66 | contrib_loop "${negid}" 67 | if [ -f "${SHARE_CHANNEL}/consensus/${consensus}" ]; then 68 | COMMAND_OUT="$(cat ${SHARE_CHANNEL}/consensus/${consensus})" 69 | else 70 | COMMAND_OUT="$(exe ${CMD} ${COMMAND} --consensus-id="${consensus}")" 71 | echo "${COMMAND_OUT}" | tee "${SHARE_CHANNEL}/consensus/${consensus}" 72 | fi 73 | export consensus_result="${COMMAND_OUT}" 74 | } 75 | 76 | write_hashes_log () { 77 | EP="$1" 78 | FILE="$2" 79 | exe ${CMD} inbox list --endpoint-id ${EP} -c message_hash -f value | ${CMD} hashes wrap > ${FILE} 80 | } 81 | -------------------------------------------------------------------------------- /demo/peer1.data/zeus: -------------------------------------------------------------------------------- 1 | { 2 | "CATALOG_URL": { 3 | "description": null, 4 | "value": "http://127.0.0.1:8000/", 5 | "title": "CATALOG_URL" 6 | }, 7 | "CRYPTO_BACKEND": { 8 | "description": null, 9 | "value": "panoramix.backends.zeus_backend", 10 | "title": "CRYPTO_BACKEND" 11 | }, 12 | "NAME": { 13 | "description": null, 14 | "value": "peer1", 15 | "title": "NAME" 16 | }, 17 | "KEY": { 18 | "description": null, 19 | "value": { 20 | "SECRET": 12345, 21 | "PUBLIC": 15440008421441127168372068809074609019832402685910570147294994852616715955978528105192237906991852285899107605111337913237414888980546601825010796966196310359430668988420197678991517680475693016693971052029882891607501380835676198592037489273810510539915102450447423960704265339831088323684194054889216028771257963581716820130053432142190929106824191067330331604477776642698470966817034346828089989654854863644811476530334633714262718527798725287617825792174661552556392306996425048736590658574986427328408283118171205025215001510304391404096280919428358980113527124722299136497795876928637431574069145485213383815033 22 | }, 23 | "title": "KEY" 24 | } 25 | } -------------------------------------------------------------------------------- /demo/peer1.data/zeus_keyid: -------------------------------------------------------------------------------- 1 | 292918e814abd7ab9e476d99bf861f016cc4ffec765dabc010fc33aab0721dfd -------------------------------------------------------------------------------- /demo/peer2.data/zeus: -------------------------------------------------------------------------------- 1 | { 2 | "CATALOG_URL": { 3 | "description": null, 4 | "value": "http://127.0.0.1:8000/", 5 | "title": "CATALOG_URL" 6 | }, 7 | "CRYPTO_BACKEND": { 8 | "description": null, 9 | "value": "panoramix.backends.zeus_backend", 10 | "title": "CRYPTO_BACKEND" 11 | }, 12 | "NAME": { 13 | "description": null, 14 | "value": "peer2", 15 | "title": "NAME" 16 | }, 17 | "KEY": { 18 | "description": null, 19 | "value": { 20 | "SECRET": 23456, 21 | "PUBLIC": 10983433001431982921630013285956744705943214689726085246838250540222232819442767349114478360032421121049780073721515458605254502009455345589320404163296752265628622976049186780446785139119859339090299004849509335355727612190551233613033060390233917439972447357030869461968915047933962039205875293812912368255098409203079598235367169085889226512605229384002238215800082970349862792749155291249567245165711713974991998547470144658381026274182937625592669670548708314230210737057880176411032899092836064767595544836500251096575019833269973193770463569818491375941561641914177094214795788050846428519317092667860735221842 22 | }, 23 | "title": "KEY" 24 | } 25 | } -------------------------------------------------------------------------------- /demo/peer2.data/zeus_keyid: -------------------------------------------------------------------------------- 1 | c303bc69c06008161352be15dad4ff06d3eea21dd4aeac6633387c8c91875aaf -------------------------------------------------------------------------------- /docker/README.rst: -------------------------------------------------------------------------------- 1 | Build image 2 | ----------- 3 | 1. Initialize code to be included in the image. We don't include 4 | the root repo dir to prevent image rebuilds for every minor 5 | change in current repo. 6 | 7 | $ ./docker/init-repos.sh 8 | $ docker build -t consensus . 9 | 10 | Create container 11 | ---------------- 12 | 13 | - Demo container 14 | 15 | $ docker run -e NRTRUSTEES= -p :9000 -ti consensus 16 | 17 | 18 | - Development container, mounts app and ui local dirs to docker container 19 | 20 | $ docker create --name consensus-dev \ 21 | -p 9000:9000 \ 22 | -e NRTRUSTEES=2 \ 23 | -v ${PWD}:/srv/app \ 24 | -v ${PWD}/ui:/srv/ui \ 25 | consensus; 26 | 27 | -------------------------------------------------------------------------------- /docker/create-dev.sh: -------------------------------------------------------------------------------- 1 | docker build -t consensus . || exit; 2 | docker stop consensus-dev; 3 | docker rm consensus-dev; 4 | docker create --name consensus-dev \ 5 | -p 9000:9000 \ 6 | -v ${PWD}:/srv/app \ 7 | -v ${PWD}/ui:/srv/ui \ 8 | -v ${PWD}/docker/services.conf:/srv/services.conf \ 9 | consensus; 10 | docker start consensus-dev; 11 | -------------------------------------------------------------------------------- /docker/init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd /srv/app 4 | ./init-consensus.sh 5 | 6 | supervisord -c /srv/services.conf -n 7 | -------------------------------------------------------------------------------- /docker/inspect.sh: -------------------------------------------------------------------------------- 1 | docker exec -ti consensus-dev /bin/zsh 2 | -------------------------------------------------------------------------------- /docker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zeus", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "Small description for zeustmp goes here", 6 | "repository": "", 7 | "license": "MIT", 8 | "author": "", 9 | "directories": { 10 | "doc": "doc", 11 | "test": "tests" 12 | }, 13 | "scripts": { 14 | "build": "ember build", 15 | "lint:hbs": "ember-template-lint .", 16 | "lint:js": "eslint .", 17 | "start": "ember serve", 18 | "test": "ember test" 19 | }, 20 | "devDependencies": { 21 | "@ember-decorators/argument": "^0.8.21", 22 | "@ember-decorators/babel-transforms": "^2.1.2", 23 | "@ember/jquery": "^0.5.2", 24 | "@ember/optional-features": "^0.6.3", 25 | "babel-eslint": "^8.2.6", 26 | "ember-cli-babel": "^6.17.2", 27 | "broccoli-asset-rev": "^2.7.0", 28 | "ember-ajax": "^3.1.0", 29 | "ember-cli": "~3.5.0", 30 | "ember-cli-app-version": "^3.2.0", 31 | "ember-cli-dependency-checker": "^3.0.0", 32 | "ember-cli-eslint": "^4.2.3", 33 | "ember-cli-htmlbars": "^3.0.0", 34 | "ember-cli-htmlbars-inline-precompile": "^1.0.3", 35 | "ember-cli-inject-live-reload": "^1.8.2", 36 | "ember-cli-sass": "^8.0.1", 37 | "ember-cli-sri": "^2.1.1", 38 | "ember-cli-template-lint": "^1.0.0-beta.1", 39 | "ember-cli-uglify": "^2.1.0", 40 | "ember-composable-helpers": "^2.1.0", 41 | "ember-data": "~3.4.4", 42 | "ember-decorators": "^2.5.2", 43 | "ember-export-application-global": "^2.0.0", 44 | "ember-load-initializers": "^1.1.0", 45 | "ember-maybe-import-regenerator": "^0.1.6", 46 | "ember-paper": "^1.0.0-beta.18", 47 | "ember-paper-expansion-panel": "0.0.4", 48 | "ember-qunit": "^3.4.1", 49 | "ember-resolver": "^5.0.1", 50 | "ember-source": "~3.5.0", 51 | "ember-truth-helpers": "^2.1.0", 52 | "eslint-plugin-ember": "^5.2.0", 53 | "loader.js": "^4.7.0", 54 | "qunit-dom": "^0.8.0", 55 | "sass": "^1.15.1" 56 | }, 57 | "engines": { 58 | "node": "6.* || 8.* || >= 10.*" 59 | }, 60 | "ember-addon": { 61 | "paths": [ 62 | "lib/apimas-docs" 63 | ] 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /docker/requirements-preheat.txt: -------------------------------------------------------------------------------- 1 | apimas-django>=0.4a3,<0.5 2 | apimas>=0.4a3,<0.5 3 | specular>=0.4a3,<0.5 4 | flask 5 | flask-cors 6 | -------------------------------------------------------------------------------- /docker/restart.sh: -------------------------------------------------------------------------------- 1 | docker exec -ti consensus-dev supervisorctl restart zeus 2 | -------------------------------------------------------------------------------- /docker/services.conf: -------------------------------------------------------------------------------- 1 | [unix_http_server] 2 | file=/var/run/supervisor.sock 3 | chmod=0700 4 | 5 | [supervisord] 6 | redirect_stderr=true 7 | childlogdir=/srv/logs/ 8 | pidfile=/srv/supervisord.pid 9 | 10 | [rpcinterface:supervisor] 11 | supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface 12 | 13 | [eventlistener:stdout] 14 | command = supervisor_stdout 15 | buffer_size = 100 16 | events = PROCESS_LOG 17 | result_handler = supervisor_stdout:event_handler 18 | 19 | [program:consensus] 20 | environment=CONSENSUS_DATABASE=/srv/db/sqlite.db 21 | directory=/srv/app/consensus/consensus-service/ 22 | command=bash -c "python manage.py migrate && python manage.py runserver --nothreading" 23 | autostart = true 24 | autorestart = true 25 | stopsignal=KILL 26 | stopasgroup=true 27 | stdout_events_enabled=true 28 | stderr_events_enabled=true 29 | 30 | [program:zeus] 31 | environment=DEBUG=http,USE_FLASK=1,SERVE=1,PYTHONPATH=. 32 | directory=/srv/app/ 33 | command=bash -c "python zeus_trust/trust.py run" 34 | autostart = true 35 | autorestart = true 36 | stopsignal=KILL 37 | stopasgroup=true 38 | stdout_events_enabled=true 39 | stderr_events_enabled=true 40 | 41 | [program:ui] 42 | environment=DEBUG=http,USE_FLASK=1,SERVE=1 43 | directory=/srv/ui/ 44 | command=bash -c "yarn install && bower --allow-root install; ./node_modules/.bin/ember build --watch --environment development" 45 | autostart = true 46 | autorestart = true 47 | stopsignal=KILL 48 | stopasgroup=true 49 | stdout_events_enabled=true 50 | stderr_events_enabled=true 51 | -------------------------------------------------------------------------------- /docker/view-logs.sh: -------------------------------------------------------------------------------- 1 | docker logs -f consensus-dev 2 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Panoramix documentation master file, created by 2 | sphinx-quickstart on Wed Jun 1 15:29:52 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 Panoramix's documentation! 7 | ===================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | panoramix_api 15 | 16 | Indices and tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | 23 | -------------------------------------------------------------------------------- /docs/mixnet_setup.rst: -------------------------------------------------------------------------------- 1 | Setting up a mixnet 2 | =================== 3 | 4 | Peer creation 5 | ------------- 6 | 7 | We first need to create a joint peer. This fixes the crypto settings across 8 | the mixnet, but does not involve any application settings. A simple 9 | negotiation is enough. 10 | 11 | Endpoint creation 12 | ----------------- 13 | 14 | In the zeus case we start off a negotiation with the following definition:: 15 | 16 | [ 17 | { 18 | "peer_id": joint_peer, 19 | "endpoint_id": "ballotbox_election_foo", 20 | "endpoint_type": "ZEUS_BALLOT_BOX", 21 | "links": [{"from_endpoint_id": "combine_foo", 22 | "from_box": "OUTBOX", 23 | "to_box": "PROCESSBOX"}] 24 | }, 25 | { 26 | "peer_id": peer1, 27 | "endpoint_id": "mix_peer1_foo", 28 | "endpoint_type": "ZEUS_SK_MIX", 29 | "links": [{"from_endpoint_id": "ballotbox_election_foo", 30 | "from_box": "ACCEPTED", 31 | "to_box": "INBOX"}] 32 | }, 33 | { 34 | "peer_id": peer2, 35 | "endpoint_id": "mix_peer2_foo", 36 | "endpoint_type": "ZEUS_SK_MIX", 37 | "links": [{"from_endpoint_id": "mix_peer1_foo", 38 | "from_box": "OUTBOX", 39 | "to_box": "INBOX"}] 40 | }, 41 | { 42 | "peer_id": peer1, 43 | "endpoint_id": "decr_peer1_foo", 44 | "endpoint_type": "ZEUS_SK_PARTIAL_DECRYPT", 45 | "links": [{"from_endpoint_id": "mix_peer2_foo", 46 | "from_box": "OUTBOX", 47 | "to_box": "INBOX"}] 48 | }, 49 | { 50 | "peer_id": peer2, 51 | "endpoint_id": "decr_peer2_foo", 52 | "endpoint_type": "ZEUS_SK_PARTIAL_DECRYPT", 53 | "links": [{"from_endpoint_id": "mix_peer2_foo", 54 | "from_box": "OUTBOX", 55 | "to_box": "INBOX"}] 56 | }, 57 | { 58 | "peer_id": joint_peer, 59 | "endpoint_id": "combine_foo", 60 | "endpoint_type": "ZEUS_SK_COMBINE", 61 | "links": [{"from_endpoint_id": "decr_peer1_foo", 62 | "from_box": "OUTBOX", 63 | "to_box": "INBOX"}, 64 | {"from_endpoint_id": "decr_peer2_foo", 65 | "from_box": "OUTBOX", 66 | "to_box": "INBOX"}] 67 | } 68 | ] 69 | 70 | This describes a graph of endpoints: each list element is a prescription to 71 | create an endpoint. The attribute "links" describes where each endpoint 72 | takes its input from (be it the input of the INBOX or the PROCESSBOX). 73 | 74 | The mixnet contributors (peers) inspect the definition, probably negotiate 75 | it in order to change eg the endpoint_ids (or any other attributes not shown 76 | in the above definition), and finally create the endpoints according to the 77 | definition. 78 | 79 | 80 | In the sphinxmix case (static routing) we start off with:: 81 | 82 | [ 83 | { 84 | "peer_id": joint_peer, 85 | "endpoint_id": "our_sphinx_mixnet", 86 | "endpoint_type": "SPHINXMIX_STATIC_GW", 87 | "size_min": 3, 88 | "links": [{"from_endpoint_id": "peer2_mix", 89 | "from_box": "OUTBOX", 90 | "to_box": "PROCESSBOX"}] 91 | }, 92 | { 93 | "peer_id": peer1, 94 | "endpoint_id": "peer1_mix", 95 | "endpoint_type": "SPHINXMIX_STATIC", 96 | "size_min": 3, 97 | "links": [{"from_endpoint_id": "our_sphinx_mixnet", 98 | "from_box": "ACCEPTED", 99 | "to_box": "INBOX"}] 100 | }, 101 | { 102 | "peer_id": peer2, 103 | "endpoint_id": "peer2_mix", 104 | "endpoint_type": "SPHINXMIX_STATIC", 105 | "size_min": 3, 106 | "links": [{"from_endpoint_id": "peer1_mix", 107 | "from_box": "OUTBOX", 108 | "to_box": "INBOX"}] 109 | } 110 | ] 111 | 112 | Working with the endpoints 113 | -------------------------- 114 | 115 | 1. End-users post messages to 'inbox' of main endpoint. 116 | 117 | 2. Owners close the endpoint. Messages move to 'accepted'. 118 | 119 | 3. Transmission: The endpoint that expects to get its input from the main 120 | endpoint, polls its status until it becomes CLOSED and then retrieves the 121 | accepted messages. 122 | 123 | 4. The running endpoint is closed, messages are processed and uploaded, and 124 | the endpoint moves to state PROCESSED. 125 | 126 | 5. Similarly, steps 3 and 4 are executed by remaining peers according to 127 | their specified links. 128 | 129 | 6. The main endpoint waits for the last endpoint to finish processing (as 130 | specified in links), then pushes the processed messages to its own 131 | processbox. After acknowledging the processed messages though a 132 | negotiation, the output of the mixnet is available at the main endpoint's 133 | outbox. 134 | -------------------------------------------------------------------------------- /panoramix-agent/panoramix/__init__.py: -------------------------------------------------------------------------------- 1 | # A namespace package. 2 | try: 3 | import pkg_resources 4 | pkg_resources.declare_namespace(__name__) 5 | except ImportError: 6 | import pkgutil 7 | __path__ = pkgutil.extend_path(__path__, __name__) 8 | -------------------------------------------------------------------------------- /panoramix-agent/panoramix/agent/__init__.py: -------------------------------------------------------------------------------- 1 | # A namespace package. 2 | try: 3 | import pkg_resources 4 | pkg_resources.declare_namespace(__name__) 5 | except ImportError: 6 | import pkgutil 7 | __path__ = pkgutil.extend_path(__path__, __name__) 8 | -------------------------------------------------------------------------------- /panoramix-agent/panoramix/agent/agent.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, abort 2 | 3 | from panoramix.config import cfg 4 | from panoramix.common import SphinxmixClient 5 | 6 | app = Flask(__name__) 7 | 8 | client = SphinxmixClient() 9 | 10 | 11 | @app.route("/send", methods=["POST"]) 12 | def send_message(): 13 | recipient = request.form["recipient"] 14 | text = request.form["message"] 15 | message = client.message_send(cfg.get('ENDPOINT_ID'), 16 | cfg.get('MIXNET_ID'), 17 | cfg.get('MIXERS'), 18 | text, 19 | recipient) 20 | return str(message['id']) 21 | 22 | 23 | def main(): 24 | client.register_catalog_url(cfg.get('CATALOG_URL')) 25 | client.register_crypto_client(cfg) 26 | app.run() 27 | 28 | if __name__ == "__main__": 29 | main() 30 | -------------------------------------------------------------------------------- /panoramix-agent/panoramix/agent/client.py: -------------------------------------------------------------------------------- 1 | from panoramix.config import cfg 2 | from panoramix.common import ui, config_file, SphinxmixClient 3 | import requests 4 | import argparse 5 | 6 | 7 | AGENT_ADDRESS = "http://127.0.0.1:5000" 8 | SEND_ADDRESS = requests.compat.urljoin(AGENT_ADDRESS, "send") 9 | 10 | 11 | def send_message(text=None, recipient=None): 12 | if recipient is None: 13 | recipient = ui.ask_value("recipient", "Message recipient") 14 | if text is None: 15 | text = ui.ask_value("text", "Message text") 16 | 17 | payload = {"recipient": recipient, 18 | "message": text} 19 | r = requests.post(SEND_ADDRESS, data=payload) 20 | if r.status_code != requests.codes.ok: 21 | ui.inform("Failed with code: %s" % r.status_code) 22 | exit() 23 | ui.inform("Sent message with id %s" % r.content) 24 | 25 | 26 | def output_cycle(cycle): 27 | client = SphinxmixClient() 28 | client.register_catalog_url(cfg.get('CATALOG_URL')) 29 | output_endpoint_id = '%s_output' % cfg.get('ENDPOINT_ID') 30 | messages = client.messages_list(output_endpoint_id, cycle=cycle) 31 | ui.inform(messages) 32 | 33 | 34 | def ack_cycle(cycle): 35 | client = SphinxmixClient() 36 | client.register_catalog_url(cfg.get('CATALOG_URL')) 37 | output_endpoint_id = '%s_output' % cfg.get('ENDPOINT_ID') 38 | r = client.cycle_set_state(output_endpoint_id, cycle, 'ACK') 39 | ui.inform(r) 40 | 41 | 42 | parser = argparse.ArgumentParser(description='Panoramix client') 43 | parser.add_argument('--output', metavar='CYCLE', type=int, 44 | help='Print output of CYCLE') 45 | parser.add_argument('--ack', metavar='CYCLE', type=int, 46 | help='Acknowledge output of CYCLE') 47 | 48 | 49 | def main(): 50 | ui.inform("Welcome to Panoramix client!") 51 | ui.inform("Configuration file is: %s" % config_file) 52 | ui.inform("Set PANORAMIX_CONFIG environment variable to override") 53 | args = parser.parse_args() 54 | if args.output is None and args.ack is None: 55 | send_message() 56 | return 57 | if args.output is not None: 58 | output_cycle(args.output) 59 | if args.ack is not None: 60 | ack_cycle(args.ack) 61 | 62 | 63 | if __name__ == "__main__": 64 | main() 65 | -------------------------------------------------------------------------------- /panoramix-agent/panoramix/agent/sphinxmix_agent.py: -------------------------------------------------------------------------------- 1 | import json 2 | from panoramix.common import ui, on, abort, SphinxmixClient, set_key_wizard 3 | from panoramix.config import cfg, config_file, ENV_CONFIG 4 | from panoramix.agent import agent 5 | 6 | TYPE = "SPHINXMIX" 7 | 8 | client = SphinxmixClient() 9 | 10 | 11 | def split_mixnet_url(mixnet_url): 12 | url = mixnet_url.rstrip('/') 13 | segments = url.rsplit('/', 2) 14 | if len(segments) != 3: 15 | abort() 16 | [catalog_url, resources, endpoint_id] = segments 17 | return catalog_url, endpoint_id 18 | 19 | 20 | def mixnet_url_process(mixnet_url): 21 | catalog_url, endpoint_id = split_mixnet_url(mixnet_url) 22 | cfg.set_value("CATALOG_URL", catalog_url) 23 | client.register_catalog_url(catalog_url) 24 | cfg.set_value("ENDPOINT_ID", endpoint_id) 25 | print endpoint_id 26 | endpoint = client.endpoint_get(endpoint_id) 27 | if endpoint["endpoint_type"] != "SPHINXMIX_GATEWAY": 28 | abort("Not a SPHINXMIX_GATEWAY.") 29 | 30 | peer_id = endpoint["peer_id"] 31 | cfg.set_value("MIXNET_ID", peer_id) 32 | peer = client.peer_get(peer_id) 33 | 34 | assert peer["crypto_backend"] == TYPE 35 | print peer['crypto_params'] 36 | crypto_params = json.loads(peer["crypto_params"]) 37 | cfg.set_value("CRYPTO_PARAMS", crypto_params) 38 | cfg.set_value("MIXERS", client.get_owners(peer)) 39 | 40 | 41 | def send_message(text=None, recipient=None): 42 | if recipient is None: 43 | recipient = ui.ask_value("recipient", "Message recipient") 44 | if text is None: 45 | text = ui.ask_value("text", "Message text") 46 | 47 | message = client.message_send(cfg.get('ENDPOINT_ID'), 48 | cfg.get('MIXNET_ID'), 49 | cfg.get('MIXERS'), 50 | text, 51 | recipient) 52 | ui.inform("Sent message with id %s" % message['id']) 53 | 54 | 55 | def main(text=None, recipient=None): 56 | ui.inform("Welcome to Panoramix sphinxmix agent!") 57 | ui.inform("Configuration file is: %s" % config_file) 58 | ui.inform("Set PANORAMIX_CONFIG environment variable to override") 59 | url = on("MIXNET_URL", 60 | lambda: ui.ask_value("MIXNET_URL", "Give sphinxmix mixnet URL")) 61 | mixnet_url_process(url) 62 | on("KEY", set_key_wizard) 63 | agent.main() 64 | 65 | 66 | if __name__ == "__main__": 67 | main() 68 | -------------------------------------------------------------------------------- /panoramix-agent/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | requests 3 | -------------------------------------------------------------------------------- /panoramix-agent/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import os 3 | 4 | CURPATH = os.path.dirname(os.path.realpath(__file__)) 5 | 6 | PACKAGE_NAME = "panoramix-agent" 7 | SHORT_DESCRIPTION = "The Panoramix Project" 8 | 9 | version_file = os.path.join(CURPATH, "..", "version.txt") 10 | with open(version_file) as f: 11 | VERSION = f.read().strip() 12 | 13 | PACKAGES_ROOT = '.' 14 | PACKAGES = find_packages(PACKAGES_ROOT) 15 | 16 | # Package meta 17 | CLASSIFIERS = [] 18 | 19 | 20 | requirements_file = os.path.join(CURPATH, "requirements.txt") 21 | with open(requirements_file) as f: 22 | INSTALL_REQUIRES = [ 23 | x.strip('\n') 24 | for x in f.readlines() 25 | if x and x[0] != '#' 26 | ] 27 | 28 | EXTRAS_REQUIRES = { 29 | } 30 | 31 | TESTS_REQUIRES = [ 32 | ] 33 | 34 | 35 | def get_all_data_files(dest_path, source_path): 36 | dest_path = dest_path.strip('/') 37 | source_path = source_path.strip('/') 38 | source_len = len(source_path) 39 | return [ 40 | ( 41 | os.path.join(dest_path, path[source_len:].strip('/')), 42 | [os.path.join(path, f) for f in files], 43 | ) 44 | for path, _, files in os.walk(source_path) 45 | ] 46 | 47 | 48 | setup( 49 | name=PACKAGE_NAME, 50 | version=VERSION, 51 | license='Affero GPL v3', 52 | author='GRNET S.A.', 53 | author_email='panoramix@dev.grnet.gr', 54 | description=SHORT_DESCRIPTION, 55 | classifiers=CLASSIFIERS, 56 | packages=PACKAGES, 57 | package_dir={'': PACKAGES_ROOT}, 58 | namespace_packages=['panoramix'], 59 | zip_safe=False, 60 | install_requires=INSTALL_REQUIRES, 61 | extras_require=EXTRAS_REQUIRES, 62 | tests_require=TESTS_REQUIRES, 63 | entry_points={ 64 | 'console_scripts': [ 65 | 'panoramix-agent = panoramix.agent.agent:main', 66 | 'sphinxmix-agent = panoramix.agent.sphinxmix_agent:main', 67 | 'panoramix-client = panoramix.agent.client:main', 68 | ], 69 | }, 70 | ) 71 | -------------------------------------------------------------------------------- /panoramix-service/panoramix_django/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grnet/panoramix/a9b788e1a073e106d5542511094fe4ac3828d4b6/panoramix-service/panoramix_django/__init__.py -------------------------------------------------------------------------------- /panoramix-service/panoramix_django/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | def main(): 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "panoramix_django.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | 24 | 25 | if __name__ == "__main__": 26 | main() 27 | -------------------------------------------------------------------------------- /panoramix-service/panoramix_django/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for panoramix_django project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.11.13. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.11/ref/settings/ 11 | """ 12 | 13 | import os 14 | db_path = os.environ.get('PANORAMIX_DATABASE', '/tmp/panoramix_db.sqlite3') 15 | 16 | # Quick-start development settings - unsuitable for production 17 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ 18 | 19 | # SECURITY WARNING: keep the secret key used in production secret! 20 | SECRET_KEY = '9a*i&&axh=uh%oxwxl7%(*7d2bw-p!u=fg6&5kg*)dl#w*kweh' 21 | 22 | # SECURITY WARNING: don't run with debug turned on in production! 23 | DEBUG = True 24 | 25 | ALLOWED_HOSTS = [] 26 | 27 | 28 | # Application definition 29 | 30 | INSTALLED_APPS = [ 31 | 'django.contrib.admin', 32 | 'django.contrib.auth', 33 | 'django.contrib.contenttypes', 34 | 'django.contrib.sessions', 35 | 'django.contrib.messages', 36 | 'django.contrib.staticfiles', 37 | 'panoramix_service', 38 | ] 39 | 40 | MIDDLEWARE = [ 41 | 'django.middleware.security.SecurityMiddleware', 42 | 'django.contrib.sessions.middleware.SessionMiddleware', 43 | 'django.middleware.common.CommonMiddleware', 44 | 'django.middleware.csrf.CsrfViewMiddleware', 45 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 46 | 'django.contrib.messages.middleware.MessageMiddleware', 47 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 48 | ] 49 | 50 | ROOT_URLCONF = 'panoramix_django.urls' 51 | 52 | TEMPLATES = [ 53 | { 54 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 55 | 'DIRS': [], 56 | 'APP_DIRS': True, 57 | 'OPTIONS': { 58 | 'context_processors': [ 59 | 'django.template.context_processors.debug', 60 | 'django.template.context_processors.request', 61 | 'django.contrib.auth.context_processors.auth', 62 | 'django.contrib.messages.context_processors.messages', 63 | ], 64 | }, 65 | }, 66 | ] 67 | 68 | WSGI_APPLICATION = 'panoramix_django.wsgi.application' 69 | 70 | 71 | # Database 72 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 73 | 74 | DATABASES = { 75 | 'default': { 76 | 'ENGINE': 'django.db.backends.sqlite3', 77 | 'NAME': db_path, 78 | } 79 | } 80 | 81 | 82 | # Password validation 83 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators 84 | 85 | AUTH_PASSWORD_VALIDATORS = [ 86 | { 87 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 88 | }, 89 | { 90 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 91 | }, 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 94 | }, 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 97 | }, 98 | ] 99 | 100 | 101 | # Internationalization 102 | # https://docs.djangoproject.com/en/1.11/topics/i18n/ 103 | 104 | LANGUAGE_CODE = 'en-us' 105 | 106 | TIME_ZONE = 'UTC' 107 | 108 | USE_I18N = True 109 | 110 | USE_L10N = True 111 | 112 | USE_TZ = True 113 | 114 | 115 | # Static files (CSS, JavaScript, Images) 116 | # https://docs.djangoproject.com/en/1.11/howto/static-files/ 117 | 118 | STATIC_URL = '/static/' 119 | -------------------------------------------------------------------------------- /panoramix-service/panoramix_django/test_settings.py: -------------------------------------------------------------------------------- 1 | from panoramix_django.settings import * 2 | 3 | import logging 4 | logging.basicConfig(level='DEBUG') 5 | 6 | DEBUG = False 7 | DEBUG_PROPAGATE_EXCEPTIONS = True 8 | -------------------------------------------------------------------------------- /panoramix-service/panoramix_django/urls.py: -------------------------------------------------------------------------------- 1 | """panoramix_django URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.11/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url 17 | from django.contrib import admin 18 | 19 | from apimas_django import provider 20 | from panoramix_django.spec import APP_CONFIG, DEPLOY_CONFIG 21 | 22 | try: 23 | app_spec = provider.configure_apimas_app(APP_CONFIG) 24 | deployment_spec = provider.configure_spec(app_spec, DEPLOY_CONFIG) 25 | 26 | api_urls = provider.construct_views(deployment_spec) 27 | except: 28 | import pdb; pdb.post_mortem() 29 | 30 | urlpatterns = [ 31 | url(r'^admin/', admin.site.urls), 32 | ] 33 | urlpatterns.extend(api_urls) 34 | -------------------------------------------------------------------------------- /panoramix-service/panoramix_django/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for panoramix_django project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "panoramix_django.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /panoramix-service/panoramix_service/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grnet/panoramix/a9b788e1a073e106d5542511094fe4ac3828d4b6/panoramix-service/panoramix_service/__init__.py -------------------------------------------------------------------------------- /panoramix-service/panoramix_service/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.apps import AppConfig 5 | 6 | 7 | class PanoramixServiceConfig(AppConfig): 8 | name = 'panoramix_service' 9 | -------------------------------------------------------------------------------- /panoramix-service/panoramix_service/logic.py: -------------------------------------------------------------------------------- 1 | from apimas.errors import ValidationError 2 | from panoramix_service import models 3 | from django.db.models import Q, F 4 | 5 | 6 | def get_endpoint_for_update(endpoint_id): 7 | try: 8 | return models.Endpoint.objects.select_for_update().get( 9 | endpoint_id=endpoint_id) 10 | except models.Endpoint.DoesNotExist: 11 | raise ValidationError() 12 | 13 | 14 | def set_cycle(backend_input, instance, context): 15 | assert instance is None 16 | kwargs = context['request/meta/kwargs'] 17 | endpoint_id = kwargs['id0'] 18 | endpoint = get_endpoint_for_update(endpoint_id) 19 | if not endpoint.public: 20 | raise ValidationError('Cannot post to a private endpoint') 21 | 22 | if endpoint.current_cycle is None: 23 | raise ValidationError('No open cycle') 24 | backend_input['cycle_id'] = endpoint.current_cycle_id 25 | 26 | 27 | def new_cycle(backend_input, endpoint_id, context): 28 | endpoint = get_endpoint_for_update(endpoint_id) 29 | current_cycle = endpoint.current_cycle 30 | new_cycle = (current_cycle.cycle if current_cycle else 0) + 1 31 | cycle = models.Cycle.objects.create( 32 | cycle=new_cycle, 33 | endpoint_id=endpoint_id, 34 | **backend_input) 35 | endpoint.current_cycle = cycle 36 | endpoint.save() 37 | return cycle 38 | 39 | 40 | def update_non_current(context): 41 | return ~Q(pk=F('endpoint__current_cycle_id')) 42 | 43 | 44 | def bulk_upload(backend_input, cycle, context): 45 | messages_input = backend_input['cycle_messages'] 46 | endpoint = cycle.endpoint 47 | if endpoint.current_cycle != cycle: 48 | raise ValidationError("Cycle is not current") 49 | messages = [] 50 | for message_input in messages_input: 51 | messages.append( 52 | models.Message(endpoint=endpoint, 53 | cycle=cycle, 54 | **message_input)) 55 | 56 | models.Message.objects.bulk_create(messages) 57 | 58 | 59 | def set_message_state(backend_input, cycle, context): 60 | objects = cycle.cycle_messages 61 | action_input = backend_input['cycle_messages'] 62 | if len(action_input) == 1 and action_input[0]['id'] == -1: 63 | state = action_input[0]['state'] 64 | else: 65 | message_ids = [msg['id'] for msg in action_input] 66 | states = set(msg['state'] for msg in action_input) 67 | if len(states) != 1: 68 | raise ValidationError("Must set the same state") 69 | state = states.pop() 70 | objects = objects.filter(id__in=message_ids) 71 | 72 | objects.update(state=state) 73 | 74 | 75 | def purge(backend_input, cycle, context): 76 | assert not backend_input 77 | endpoint = cycle.endpoint 78 | if endpoint.current_cycle == cycle: 79 | raise ValidationError('Cannot purge the current cycle') 80 | cycle.cycle_messages.all().delete() 81 | -------------------------------------------------------------------------------- /panoramix-service/panoramix_service/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.16 on 2018-12-21 10:30 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Cycle', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('cycle', models.IntegerField()), 22 | ('state', models.CharField(db_index=True, max_length=255)), 23 | ], 24 | ), 25 | migrations.CreateModel( 26 | name='Endpoint', 27 | fields=[ 28 | ('endpoint_id', models.CharField(max_length=255, primary_key=True, serialize=False)), 29 | ('peer_id', models.CharField(db_index=True, max_length=255)), 30 | ('owner', models.CharField(max_length=255)), 31 | ('description', models.CharField(max_length=255)), 32 | ('public', models.BooleanField()), 33 | ('endpoint_type', models.CharField(max_length=255)), 34 | ('endpoint_params', models.TextField()), 35 | ('current_cycle', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='current_of', to='panoramix_service.Cycle')), 36 | ], 37 | ), 38 | migrations.CreateModel( 39 | name='Message', 40 | fields=[ 41 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 42 | ('serial', models.IntegerField(null=True)), 43 | ('sender', models.CharField(max_length=255)), 44 | ('recipient', models.CharField(max_length=255)), 45 | ('text', models.TextField()), 46 | ('state', models.CharField(db_index=True, max_length=255)), 47 | ('cycle', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cycle_messages', to='panoramix_service.Cycle')), 48 | ('endpoint', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='endpoint_messages', to='panoramix_service.Endpoint')), 49 | ], 50 | ), 51 | migrations.CreateModel( 52 | name='Owner', 53 | fields=[ 54 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 55 | ('position', models.IntegerField()), 56 | ('owner_key_id', models.CharField(max_length=255)), 57 | ], 58 | ), 59 | migrations.CreateModel( 60 | name='Peer', 61 | fields=[ 62 | ('name', models.CharField(max_length=255)), 63 | ('peer_id', models.CharField(max_length=255, primary_key=True, serialize=False)), 64 | ('key_type', models.IntegerField()), 65 | ('crypto_backend', models.CharField(max_length=255)), 66 | ('crypto_params', models.TextField()), 67 | ('key_data', models.TextField(unique=True)), 68 | ('status', models.CharField(max_length=255)), 69 | ], 70 | ), 71 | migrations.AddField( 72 | model_name='owner', 73 | name='peer', 74 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='owners', to='panoramix_service.Peer'), 75 | ), 76 | migrations.AddField( 77 | model_name='cycle', 78 | name='endpoint', 79 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cycles', to='panoramix_service.Endpoint'), 80 | ), 81 | migrations.AlterUniqueTogether( 82 | name='owner', 83 | unique_together=set([('peer', 'owner_key_id')]), 84 | ), 85 | migrations.AlterUniqueTogether( 86 | name='cycle', 87 | unique_together=set([('endpoint', 'cycle')]), 88 | ), 89 | ] 90 | -------------------------------------------------------------------------------- /panoramix-service/panoramix_service/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grnet/panoramix/a9b788e1a073e106d5542511094fe4ac3828d4b6/panoramix-service/panoramix_service/migrations/__init__.py -------------------------------------------------------------------------------- /panoramix-service/panoramix_service/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models 5 | 6 | 7 | class Peer(models.Model): 8 | name = models.CharField(max_length=255) 9 | peer_id = models.CharField(max_length=255, primary_key=True) 10 | key_type = models.IntegerField() 11 | crypto_backend = models.CharField(max_length=255) 12 | crypto_params = models.TextField() 13 | key_data = models.TextField(unique=True) 14 | status = models.CharField(max_length=255) 15 | 16 | 17 | class Owner(models.Model): 18 | peer = models.ForeignKey(Peer, related_name='owners') 19 | position = models.IntegerField() 20 | owner_key_id = models.CharField(max_length=255) 21 | 22 | class Meta: 23 | unique_together = ["peer", "owner_key_id"] 24 | 25 | 26 | class Endpoint(models.Model): 27 | endpoint_id = models.CharField(max_length=255, primary_key=True) 28 | peer_id = models.CharField(max_length=255, db_index=True) 29 | owner = models.CharField(max_length=255) 30 | description = models.CharField(max_length=255) 31 | public = models.BooleanField() 32 | endpoint_type = models.CharField(max_length=255) 33 | endpoint_params = models.TextField() 34 | current_cycle = models.ForeignKey( 35 | 'Cycle', null=True, related_name='current_of') 36 | 37 | @property 38 | def current_cycle_property(self): 39 | if self.current_cycle is None: 40 | return 0 41 | return self.current_cycle.cycle 42 | 43 | 44 | class Cycle(models.Model): 45 | endpoint = models.ForeignKey(Endpoint, related_name='cycles') 46 | cycle = models.IntegerField() 47 | state = models.CharField(max_length=255, db_index=True) 48 | 49 | @property 50 | def message_count(self): 51 | return self.cycle_messages.all().count() 52 | 53 | class Meta: 54 | unique_together = ["endpoint", "cycle"] 55 | 56 | 57 | class Message(models.Model): 58 | endpoint = models.ForeignKey(Endpoint, related_name='endpoint_messages') 59 | cycle = models.ForeignKey(Cycle, related_name='cycle_messages') 60 | serial = models.IntegerField(null=True) 61 | sender = models.CharField(max_length=255) # some peer 62 | recipient = models.CharField(max_length=255) # some peer 63 | text = models.TextField() 64 | # message_hash = models.CharField(max_length=255) 65 | state = models.CharField(max_length=255, db_index=True) 66 | 67 | class Meta: 68 | pass 69 | # unique_together = ["endpoint_id", "box", "message_hash"] 70 | # index_together = ["endpoint_id", "box", "id"] 71 | -------------------------------------------------------------------------------- /panoramix-service/panoramix_service/rules.py: -------------------------------------------------------------------------------- 1 | # COLUMNS = ('collection', 'action', 'role', 'filter', 'check', 'fields', 'comment') 2 | 3 | RULES = [ 4 | ('panoramix/peers', 'create', '*', '*', '*', '*', ''), 5 | ('panoramix/peers', 'list', '*', '*', '*', '*', ''), 6 | ('panoramix/peers', 'retrieve', '*', '*', '*', '*', ''), 7 | 8 | ('panoramix/endpoints', 'create', '*', '*', '*', '*', ''), 9 | ('panoramix/endpoints', 'list', '*', '*', '*', '*', ''), 10 | ('panoramix/endpoints', 'retrieve', '*', '*', '*', '*', ''), 11 | 12 | ('panoramix/endpoints/messages', 'create', '*', 13 | '*', 'set_cycle', 'sender,recipient,text', ''), 14 | 15 | ('panoramix/endpoints/messages', 'retrieve', '*', '*', '*', '*', ''), 16 | ('panoramix/endpoints/messages', 'list', '*', '*', '*', '*', ''), 17 | 18 | ('panoramix/endpoints/cycles', 'create', '*', 19 | '*', '*', 'state', ''), 20 | 21 | ('panoramix/endpoints/cycles', 'retrieve', '*', 22 | '*', '*', 'cycle,state,message_count', ''), 23 | 24 | # ('panoramix/endpoints/cycles', 'list', '*', 25 | # '*', '*', 'cycle,state,message_count', ''), 26 | 27 | ('panoramix/endpoints/cycles', 'list', '*', 28 | '*', '*', '*', ''), 29 | 30 | ('panoramix/endpoints/cycles', 'partial_update', '*', 31 | 'update_non_current', '*', 'state', ''), 32 | 33 | ('panoramix/endpoints/cycles', 'bulk-upload', '*', 34 | '*', '*', 'messages/sender,messages/recipient,messages/text,messages/state', ''), 35 | 36 | ('panoramix/endpoints/cycles', 'set-message-state', '*', 37 | '*', '*', 'messages/id,messages/state', ''), 38 | 39 | ('panoramix/endpoints/cycles', 'purge', '*', 40 | '*', '*', 'cycle', ''), 41 | 42 | ] 43 | 44 | def get_rules(): 45 | return RULES 46 | -------------------------------------------------------------------------------- /panoramix-service/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | python_files= 3 | test.py 4 | DJANGO_SETTINGS_MODULE=panoramix_django.test_settings 5 | 6 | -------------------------------------------------------------------------------- /panoramix-service/requirements.txt: -------------------------------------------------------------------------------- 1 | apimas-django>=0.4a3,<0.5 2 | -------------------------------------------------------------------------------- /panoramix-service/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import os 3 | 4 | CURPATH = os.path.dirname(os.path.realpath(__file__)) 5 | 6 | PACKAGE_NAME = "panoramix-service" 7 | SHORT_DESCRIPTION = "The Panoramix Project" 8 | 9 | version_file = os.path.join(CURPATH, "..", "version.txt") 10 | with open(version_file) as f: 11 | VERSION = f.read().strip() 12 | 13 | PACKAGES_ROOT = '.' 14 | PACKAGES = find_packages(PACKAGES_ROOT) 15 | 16 | # Package meta 17 | CLASSIFIERS = [] 18 | 19 | 20 | requirements_file = os.path.join(CURPATH, "requirements.txt") 21 | with open(requirements_file) as f: 22 | INSTALL_REQUIRES = [ 23 | x.strip('\n') 24 | for x in f.readlines() 25 | if x and x[0] != '#' 26 | ] 27 | 28 | EXTRAS_REQUIRES = { 29 | } 30 | 31 | TESTS_REQUIRES = [ 32 | ] 33 | 34 | 35 | def get_all_data_files(dest_path, source_path): 36 | dest_path = dest_path.strip('/') 37 | source_path = source_path.strip('/') 38 | source_len = len(source_path) 39 | return [ 40 | ( 41 | os.path.join(dest_path, path[source_len:].strip('/')), 42 | [os.path.join(path, f) for f in files], 43 | ) 44 | for path, _, files in os.walk(source_path) 45 | ] 46 | 47 | 48 | setup( 49 | name=PACKAGE_NAME, 50 | version=VERSION, 51 | license='Affero GPL v3', 52 | author='GRNET S.A.', 53 | author_email='panoramix@dev.grnet.gr', 54 | description=SHORT_DESCRIPTION, 55 | classifiers=CLASSIFIERS, 56 | packages=PACKAGES, 57 | package_dir={'': PACKAGES_ROOT}, 58 | zip_safe=False, 59 | install_requires=INSTALL_REQUIRES, 60 | extras_require=EXTRAS_REQUIRES, 61 | tests_require=TESTS_REQUIRES, 62 | 63 | entry_points={ 64 | 'console_scripts': [ 65 | 'panoramix-manage = panoramix_django.manage:main', 66 | ], 67 | }, 68 | ) 69 | -------------------------------------------------------------------------------- /panoramix-service/test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from apimas_django.test import * 4 | from panoramix_service import models 5 | 6 | pytestmark = pytest.mark.django_db(transaction=False) 7 | 8 | 9 | def test_workflow(client): 10 | api = client.copy(prefix='/panoramix/') 11 | 12 | data = { 13 | 'peer_id': 'peer1_id', 14 | 'name': 'name1', 15 | 'key_type': 1, 16 | 'key_data': 'key_data', 17 | 'crypto_backend': 'sphinxmix', 18 | 'crypto_params': '', 19 | } 20 | r = api.post('peers', data) 21 | assert r.status_code == 201 22 | 23 | end1_id = 'end1_id' 24 | data = { 25 | 'endpoint_id': end1_id, 26 | 'peer_id': 'peer1_id', 27 | 'public': True, 28 | 'description': 'ddd', 29 | 'endpoint_type': 'type', 30 | 'endpoint_params': 'params', 31 | 'public': 1, 32 | } 33 | r = api.post('endpoints', data) 34 | assert r.status_code == 201 35 | assert r.json()['current_cycle'] == 0 36 | 37 | end1_path = 'endpoints/%s' % end1_id 38 | r = api.post('%s/cycles/' % end1_path, {}) 39 | assert r.status_code == 201 40 | 41 | r = api.get(end1_path) 42 | assert r.status_code == 200 43 | assert r.json()['current_cycle'] == 1 44 | 45 | r = api.get('%s/cycles/1' % end1_path) 46 | assert r.status_code == 200 47 | 48 | end2_id = 'end2_id' 49 | data = { 50 | 'endpoint_id': end2_id, 51 | 'peer_id': 'peer1_id', 52 | 'public': True, 53 | 'description': 'ddd', 54 | 'endpoint_type': 'type', 55 | 'endpoint_params': 'params', 56 | 'public': 1, 57 | } 58 | r = api.post('endpoints', data) 59 | assert r.status_code == 201 60 | 61 | end2_path = 'endpoints/%s' % end2_id 62 | r = api.post('%s/cycles/' % end2_path, {'state': 'new'}) 63 | assert r.status_code == 201 64 | assert r.json()['cycle'] == 1 65 | 66 | r = api.get('%s/cycles/1/' % end2_path) 67 | assert r.status_code == 200 68 | 69 | data = { 70 | 'sender': 'sender1', 71 | 'recipient': 'recipient1', 72 | 'text': 'text1', 73 | } 74 | 75 | r = api.post('%s/messages/' % end1_path, data) 76 | assert r.status_code == 201 77 | body = r.json() 78 | m1_id = body['id'] 79 | assert body['state'] == 'INBOX' 80 | 81 | data = { 82 | 'sender': 'sender2', 83 | 'recipient': 'recipient2', 84 | 'text': 'text2', 85 | } 86 | 87 | r = api.post('%s/messages/' % end1_path, data) 88 | assert r.status_code == 201 89 | body = r.json() 90 | m2_id = body['id'] 91 | 92 | data = { 93 | 'sender': 'sender3', 94 | 'recipient': 'recipient3', 95 | 'text': 'text3', 96 | } 97 | 98 | r = api.post('%s/messages/' % end1_path, data) 99 | assert r.status_code == 201 100 | body = r.json() 101 | m3_id = body['id'] 102 | 103 | r = api.get('%s/messages/' % end1_path, {'flt__cycle': 1}) 104 | assert r.status_code == 200 105 | assert len(r.json()) == 3 106 | 107 | r = api.get('%s/cycles/1/' % end1_path) 108 | assert r.status_code == 200 109 | assert r.json()['message_count'] == 3 110 | 111 | r = api.get('%s/messages/' % end1_path, {'flt__cycle': 2}) 112 | assert r.status_code == 200 113 | assert len(r.json()) == 0 114 | 115 | r = api.post('%s/cycles/' % end1_path, {}) 116 | assert r.status_code == 201 117 | assert r.json()['cycle'] == 2 118 | 119 | data = { 120 | 'sender': 'sender3', 121 | 'recipient': 'recipient3', 122 | 'text': 'text3', 123 | } 124 | 125 | r = api.post('%s/messages/' % end1_path, data) 126 | assert r.status_code == 201 127 | assert r.json()['cycle'] == 2 128 | 129 | data = { 130 | 'messages': [ 131 | {'id': m1_id, 'state': 'accepted'}, 132 | {'id': m2_id, 'state': 'accepted'}, 133 | ], 134 | } 135 | r = api.post('%s/cycles/1/set-message-state/' % end1_path, data) 136 | assert r.status_code == 200 137 | 138 | r = api.get('%s/messages/' % end1_path, 139 | {'flt__cycle': 1, 'flt__state': 'accepted'}) 140 | assert r.status_code == 200 141 | body = r.json() 142 | assert len(body) == 2 143 | 144 | messages = [] 145 | for msg in body: 146 | messages.append( 147 | {'sender': msg['sender'], 148 | 'recipient': msg['recipient'], 149 | 'text': msg['text'], 150 | 'state': 'processed'}) 151 | 152 | data = {'messages': messages} 153 | r = api.post('%s/cycles/1/bulk-upload/' % end2_path, data) 154 | assert r.status_code == 201 155 | 156 | r = api.get('%s/messages/' % end2_path, {'flt__cycle': 1}) 157 | assert r.status_code == 200 158 | 159 | r = api.post('%s/cycles/2/purge/' % end1_path, {}) 160 | assert r.status_code == 400 161 | 162 | r = api.get('%s/messages/' % end1_path, {'flt__cycle': 2}) 163 | assert r.status_code == 200 164 | assert len(r.json()) == 1 165 | 166 | r = api.post('%s/cycles/1/purge/' % end1_path, {}) 167 | assert r.status_code == 200 168 | 169 | r = api.get('%s/messages/' % end1_path, {'flt__cycle': 1}) 170 | assert r.status_code == 200 171 | assert len(r.json()) == 0 172 | -------------------------------------------------------------------------------- /panoramix/panoramix/__init__.py: -------------------------------------------------------------------------------- 1 | # A namespace package. 2 | try: 3 | import pkg_resources 4 | pkg_resources.declare_namespace(__name__) 5 | except ImportError: 6 | import pkgutil 7 | __path__ = pkgutil.extend_path(__path__, __name__) 8 | -------------------------------------------------------------------------------- /panoramix/panoramix/backends/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grnet/panoramix/a9b788e1a073e106d5542511094fe4ac3828d4b6/panoramix/panoramix/backends/__init__.py -------------------------------------------------------------------------------- /panoramix/panoramix/canonical.py: -------------------------------------------------------------------------------- 1 | from cStringIO import StringIO 2 | 3 | 4 | def to_canonical(obj, out=None): 5 | toplevel = 0 6 | if out is None: 7 | toplevel = 1 8 | out = StringIO() 9 | if isinstance(obj, basestring): 10 | if isinstance(obj, unicode): 11 | obj = obj.encode('utf-8') 12 | z = len(obj) 13 | x = "%x" % z 14 | w = ("%02x" % len(x))[:2] 15 | out.write("%s%s_" % (w, x)) 16 | out.write(obj) 17 | elif isinstance(obj, int) or isinstance(obj, long): 18 | s = "%x" % obj 19 | z = len(s) 20 | x = "%x" % z 21 | w = ("%02x" % len(x))[:2] 22 | out.write("%s%s0%s" % (w, x, s)) 23 | elif isinstance(obj, dict): 24 | out.write('{\x0a') 25 | cobj = {} 26 | for k, v in obj.iteritems(): 27 | if not isinstance(k, str): 28 | if isinstance(k, unicode): 29 | k = k.encode('utf-8') 30 | elif isinstance(k, int) or isinstance(k, long): 31 | k = str(k) 32 | else: 33 | m = "Unsupported dict key type '%s'" % (type(k),) 34 | cobj[k] = v 35 | del obj 36 | keys = cobj.keys() 37 | keys.sort() 38 | prev = None 39 | for k in keys: 40 | if prev is not None: 41 | out.write(',\x0a') 42 | if k == prev: 43 | tail = '...' if len(k) > 64 else '' 44 | m = "duplicate key '%s' in dict" % (k[:64] + tail,) 45 | raise AssertionError(m) 46 | to_canonical(k, out=out) 47 | out.write(': ') 48 | to_canonical(cobj[k], out=out) 49 | prev = k 50 | out.write('}\x0a') 51 | elif isinstance(obj, list) or isinstance(obj, tuple): 52 | out.write('[\x0a') 53 | iterobj = iter(obj) 54 | for o in iterobj: 55 | to_canonical(o, out=out) 56 | break 57 | for o in iterobj: 58 | out.write(',\x0a') 59 | to_canonical(o, out=out) 60 | out.write(']\x0a') 61 | elif obj is None: 62 | out.write('null') 63 | else: 64 | m = "to_canonical: invalid object type '%s'" % (type(obj),) 65 | raise AssertionError(m) 66 | 67 | if toplevel: 68 | out.seek(0) 69 | return out.read() 70 | 71 | def from_canonical(inp, unicode_strings=0, s=''): 72 | if isinstance(inp, str): 73 | inp = StringIO(inp) 74 | 75 | read = inp.read 76 | if not s: 77 | s = read(2) 78 | 79 | if s == 'nu': 80 | s += read(2) 81 | if s == 'null': 82 | return None 83 | else: 84 | m = ("byte %d: invalid token '%s' instead of 'null'" 85 | % (inp.tell(), s)) 86 | raise ValueError(m) 87 | 88 | if s == '[\x0a': 89 | obj = [] 90 | append = obj.append 91 | while 1: 92 | s = read(2) 93 | if not s: 94 | m = "byte %d: eof within a list" % inp.tell() 95 | raise ValueError(m) 96 | 97 | if s == ']\x0a': 98 | return obj 99 | 100 | item = from_canonical(inp, unicode_strings=unicode_strings, s=s) 101 | append(item) 102 | 103 | s = read(2) 104 | if s == ']\x0a': 105 | return obj 106 | 107 | if s != ',\x0a': 108 | m = ("byte %d: in list: illegal token '%s' instead of ',\\n'" 109 | % (inp.tell(), s)) 110 | raise ValueError(m) 111 | 112 | if s == '{\x0a': 113 | obj = {} 114 | while 1: 115 | s = read(2) 116 | if not s: 117 | m = "byte %d: eof within dict" % inp.tell() 118 | raise ValueError(m) 119 | 120 | if s == '}\x0a': 121 | return obj 122 | 123 | key = from_canonical(inp, unicode_strings=unicode_strings, s=s) 124 | s = read(2) 125 | if s != ': ': 126 | m = ("byte %d: invalid token '%s' instead of ': '" 127 | % (inp.tell(), s)) 128 | raise ValueError(m) 129 | 130 | value = from_canonical(inp, unicode_strings=unicode_strings) 131 | obj[key] = value # allow key TypeError rise through 132 | 133 | s = read(2) 134 | if not s: 135 | m = "byte %d: eof inside dict" % inp.tell() 136 | raise ValueError(m) 137 | 138 | if s == '}\x0a': 139 | return obj 140 | 141 | if s != ',\x0a': 142 | m = ("byte %d: illegal token '%s' in dict instead of ',\\n'" 143 | % (inp.tell(), s)) 144 | raise ValueError(m) 145 | 146 | w = int(s, 16) 147 | s = read(w) 148 | if len(s) != w: 149 | m = "byte %d: eof while reading header size %d" % (inp.tell(), w) 150 | raise ValueError(m) 151 | 152 | z = int(s, 16) 153 | c = read(1) 154 | if not c: 155 | m = "byte %d: eof while reading object tag" % inp.tell() 156 | raise ValueError(m) 157 | 158 | s = read(z) 159 | if len(s) != z: 160 | m = "byte %d: eof while reading object size %d" % (inp.tell(), z) 161 | raise ValueError(m) 162 | 163 | if c == '_': 164 | if unicode_strings: 165 | try: 166 | s = s.decode('utf-8') 167 | except UnicodeDecodeError: 168 | pass 169 | return s 170 | elif c == '0': 171 | num = int(s, 16) 172 | return num 173 | else: 174 | m = "byte %d: invalid object tag '%d'" % (inp.tell()-z, c) 175 | raise ValueError(m) 176 | 177 | 178 | def from_unicode_canonical(inp): 179 | if isinstance(inp, unicode): 180 | inp = inp.encode('utf-8') 181 | return from_canonical(inp, unicode_strings=True) 182 | -------------------------------------------------------------------------------- /panoramix/panoramix/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import importlib 4 | from collections import OrderedDict 5 | 6 | 7 | class Config(object): 8 | def __init__(self, config_file): 9 | self.config_file = config_file 10 | self._cfg = None 11 | 12 | def load(self): 13 | if os.path.exists(self.config_file): 14 | with open(self.config_file) as f: 15 | cfg_s = f.read() 16 | else: 17 | cfg_s = '' 18 | return json.loads(cfg_s, object_pairs_hook=OrderedDict)\ 19 | if cfg_s else OrderedDict() 20 | 21 | def cfg(self): 22 | if self._cfg is None: 23 | self._cfg = self.load() 24 | return self._cfg 25 | 26 | def get(self, key, default=None): 27 | d = self.cfg().get(key) 28 | if d is None: 29 | if default is not None: 30 | return default 31 | return None 32 | return d["value"] 33 | 34 | def save(self): 35 | _cfg = self._cfg 36 | if _cfg is None: 37 | return 38 | with open(self.config_file, "w") as f: 39 | json.dump(_cfg, f, indent=2) 40 | 41 | def set_value(self, key, value, description=None): 42 | cfg = self.cfg() 43 | cfg[key] = { 44 | "value": value, 45 | "title": key, 46 | "description": description 47 | } 48 | self.save() 49 | 50 | def copy_to(self, key): 51 | return lambda value: self.set_value(key, value) 52 | 53 | def pop(self, key): 54 | cfg = self.cfg() 55 | d = cfg.pop(key, None) 56 | self.save() 57 | return d["value"] 58 | 59 | 60 | ENV_HOME = os.environ.get('HOME', '.') 61 | ENV_CONFIG = "PANORAMIX_CONFIG" 62 | config_file = os.environ.get( 63 | ENV_CONFIG, 64 | os.path.join(ENV_HOME, '.panoramix.config')) 65 | 66 | cfg = Config(config_file) 67 | 68 | 69 | def get_backend(): 70 | CRYPTO_BACKEND = cfg.get("CRYPTO_BACKEND") 71 | if CRYPTO_BACKEND is None: 72 | raise ValueError("CRYPTO_BACKEND is not set.") 73 | return importlib.import_module(CRYPTO_BACKEND) 74 | 75 | 76 | def get_server_backend(): 77 | crypto_module = get_backend() 78 | return crypto_module.get_server(cfg) 79 | -------------------------------------------------------------------------------- /panoramix/panoramix/interface.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | Message = namedtuple("Message", ["sender", "recipient", "body"]) 4 | 5 | 6 | class Mixnet(object): 7 | pass 8 | -------------------------------------------------------------------------------- /panoramix/panoramix/ui_term.py: -------------------------------------------------------------------------------- 1 | from panoramix.config import cfg 2 | from panoramix.utils import unicode_to_locale 3 | 4 | 5 | def inform(message): 6 | print unicode_to_locale(message) 7 | 8 | 9 | def ask_value(title, description): 10 | prompt = "%s\n%s: " % ( 11 | unicode_to_locale(description), unicode_to_locale(title)) 12 | value = raw_input(prompt).strip() 13 | return value 14 | 15 | 16 | def get_values(): 17 | return cfg.cfg() 18 | -------------------------------------------------------------------------------- /panoramix/panoramix/ui_web.py: -------------------------------------------------------------------------------- 1 | from panoramix.config import cfg 2 | 3 | 4 | def inform(message): 5 | pass 6 | 7 | 8 | def ask_value(title, description): 9 | value = cfg.get(title, None) 10 | if value is not None: 11 | return value 12 | 13 | cfg.set_value(title, None, description=description) 14 | raise StopIteration 15 | 16 | 17 | def get_values(): 18 | return cfg.cfg() 19 | -------------------------------------------------------------------------------- /panoramix/panoramix/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import hashlib 3 | import locale 4 | import itertools 5 | import base64 6 | import random 7 | 8 | system_random = random.SystemRandom() 9 | 10 | ENCODING = locale.getpreferredencoding() or 'UTF-8' 11 | 12 | 13 | def hash_string(s): 14 | return hashlib.sha256(s).hexdigest() 15 | 16 | 17 | def show_serial(serial): 18 | s = "" if serial is None else str(serial) 19 | s += "|" 20 | return s 21 | 22 | 23 | def hash_message(text, sender, recipient, serial): 24 | hasher = hashlib.sha256() 25 | hasher.update(show_serial(serial)) 26 | hasher.update(utf8(text)) 27 | hasher.update(utf8(sender)) 28 | hasher.update(utf8(recipient)) 29 | return hasher.hexdigest() 30 | 31 | 32 | def utf8(s): 33 | if type(s) is unicode: 34 | return s.encode("UTF-8") 35 | return s 36 | 37 | 38 | def locale_to_unicode(s): 39 | if type(s) is unicode: 40 | return s 41 | return unicode(s, ENCODING) 42 | 43 | 44 | def unicode_to_locale(s): 45 | if type(s) is unicode: 46 | return s.encode(ENCODING) 47 | return s 48 | 49 | 50 | def int_to_unicode(i): 51 | return unicode('%x' % i) 52 | 53 | 54 | def unicode_to_int(u): 55 | return int(u, base=16) 56 | 57 | 58 | def unzip(lst): 59 | aa = [] 60 | bb = [] 61 | for a, b in lst: 62 | aa.append(a) 63 | bb.append(b) 64 | return aa, bb 65 | 66 | 67 | def with_recipient(messages, default=None): 68 | return zip(itertools.repeat(default), messages) 69 | 70 | 71 | def generate_random_key(): 72 | s = os.urandom(32) 73 | return base64.urlsafe_b64encode(s).rstrip('=') 74 | 75 | 76 | def secure_shuffle(lst): 77 | random.shuffle(lst, random=system_random.random) 78 | 79 | 80 | class NoProcessing(Exception): 81 | pass 82 | -------------------------------------------------------------------------------- /panoramix/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grnet/panoramix/a9b788e1a073e106d5542511094fe4ac3828d4b6/panoramix/requirements.txt -------------------------------------------------------------------------------- /panoramix/requirements_gpg.txt: -------------------------------------------------------------------------------- 1 | gnupg 2 | -------------------------------------------------------------------------------- /panoramix/requirements_sphinxmix.txt: -------------------------------------------------------------------------------- 1 | sphinxmix==0.0.7 2 | -------------------------------------------------------------------------------- /panoramix/requirements_zeus.txt: -------------------------------------------------------------------------------- 1 | pycrypto 2 | gmpy 3 | -------------------------------------------------------------------------------- /panoramix/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import os 3 | 4 | CURPATH = os.path.dirname(os.path.realpath(__file__)) 5 | 6 | PACKAGE_NAME = "panoramix" 7 | SHORT_DESCRIPTION = "The Panoramix Project" 8 | 9 | version_file = os.path.join(CURPATH, "..", "version.txt") 10 | with open(version_file) as f: 11 | VERSION = f.read().strip() 12 | 13 | PACKAGES_ROOT = '.' 14 | PACKAGES = find_packages(PACKAGES_ROOT) 15 | 16 | # Package meta 17 | CLASSIFIERS = [] 18 | 19 | 20 | def read_requirements(filename): 21 | with open(filename) as f: 22 | return [ 23 | x.strip('\n') 24 | for x in f.readlines() 25 | if x and x[0] != '#' 26 | ] 27 | 28 | 29 | def from_file(extra=None): 30 | suffix = "_%s" % extra if extra else "" 31 | filename = "requirements%s.txt" % suffix 32 | return os.path.join(CURPATH, filename) 33 | 34 | 35 | INSTALL_REQUIRES = read_requirements(from_file()) 36 | 37 | 38 | EXTRAS = ["sphinxmix", "gpg", "zeus"] 39 | EXTRAS_REQUIRES = {extra: read_requirements(from_file(extra)) 40 | for extra in EXTRAS} 41 | 42 | TESTS_REQUIRES = [ 43 | ] 44 | 45 | 46 | setup( 47 | name=PACKAGE_NAME, 48 | version=VERSION, 49 | license='Affero GPL v3', 50 | author='GRNET S.A.', 51 | author_email='panoramix@dev.grnet.gr', 52 | description=SHORT_DESCRIPTION, 53 | classifiers=CLASSIFIERS, 54 | packages=PACKAGES, 55 | package_dir={'': PACKAGES_ROOT}, 56 | namespace_packages=['panoramix'], 57 | zip_safe=False, 58 | install_requires=INSTALL_REQUIRES, 59 | extras_require=EXTRAS_REQUIRES, 60 | tests_require=TESTS_REQUIRES, 61 | 62 | entry_points={ 63 | 'console_scripts': [ 64 | 'panoramix-wizard = panoramix.wizard:main', 65 | ], 66 | }, 67 | ) 68 | -------------------------------------------------------------------------------- /run_test.sh: -------------------------------------------------------------------------------- 1 | pytest --nomigrations --pdb -s panoramix-service 2 | -------------------------------------------------------------------------------- /test/mixing/config/admin: -------------------------------------------------------------------------------- 1 | { 2 | "CATALOG_URL": { 3 | "description": null, 4 | "value": "http://127.0.0.1:8000/panoramix/", 5 | "title": "CATALOG_URL" 6 | }, 7 | "CREATE_CRYPTO_PARAMS": { 8 | "description": null, 9 | "value": { 10 | "BODY_LEN": 1024, 11 | "GROUP": 713, 12 | "HEADER_LEN": 192 13 | }, 14 | "title": "CREATE_CRYPTO_PARAMS" 15 | }, 16 | "CRYPTO_PARAMS": { 17 | "description": null, 18 | "value": { 19 | "BODY_LEN": 1024, 20 | "GROUP": 713, 21 | "HEADER_LEN": 192 22 | }, 23 | "title": "CRYPTO_PARAMS" 24 | }, 25 | "KEY": { 26 | "description": null, 27 | "value": { 28 | "SECRET": "17F294F61FBD76D11EA75708B172ED73A4BA2AC464BA3E500FA7FCE1", 29 | "PUBLIC": "03dbb592837c9f8647063f0508b086fbf4971c9f3c59e3a412d6a45be4" 30 | }, 31 | "title": "KEY" 32 | }, 33 | "PEER_NAME": { 34 | "description": null, 35 | "value": "p1", 36 | "title": "PEER_NAME" 37 | }, 38 | "PEER_ID": { 39 | "description": null, 40 | "value": "03dbb592837c9f8647063f0508b086fbf4971c9f3c59e3a412d6a45be4", 41 | "title": "PEER_ID" 42 | }, 43 | "SETUP_ROLE": { 44 | "description": null, 45 | "value": "admin", 46 | "title": "SETUP_ROLE" 47 | }, 48 | "MIXERS_LIST": { 49 | "description": null, 50 | "value": [ 51 | "03dbb592837c9f8647063f0508b086fbf4971c9f3c59e3a412d6a45be4", 52 | "021729f05f0d09548a05b7c41fe4dbea98c02873b50b1ed0d2900c9800" 53 | ], 54 | "title": "MIXERS_LIST" 55 | }, 56 | "MIXNET_PEER": { 57 | "description": null, 58 | "value": "0327a4eb34641189269ccd9af8b18b06fefdef0edb831ebec0f06f0ba6", 59 | "title": "MIXNET_PEER" 60 | }, 61 | "MIXNET_NAME": { 62 | "description": null, 63 | "value": "mix", 64 | "title": "MIXNET_NAME" 65 | }, 66 | "MIN_SIZE": { 67 | "description": null, 68 | "value": 100, 69 | "title": "MIN_SIZE" 70 | }, 71 | "MIXNET_SPEC": { 72 | "description": null, 73 | "value": { 74 | "mix_endpoints": { 75 | "021729f05f0d09548a05b7c41fe4dbea98c02873b50b1ed0d2900c9800": { 76 | "endpoint_type": "SPHINXMIX", 77 | "link": { 78 | "from_state": "ACCEPTED", 79 | "from_endpoint_id": "03dbb59_for_ep_mix" 80 | }, 81 | "description": "Mixing node for 'mix'", 82 | "endpoint_id": "021729f_for_ep_mix", 83 | "endpoint_params": "{\n}\n", 84 | "admin": "021729f05f0d09548a05b7c41fe4dbea98c02873b50b1ed0d2900c9800", 85 | "peer_id": "021729f05f0d09548a05b7c41fe4dbea98c02873b50b1ed0d2900c9800", 86 | "public": false 87 | }, 88 | "03dbb592837c9f8647063f0508b086fbf4971c9f3c59e3a412d6a45be4": { 89 | "endpoint_type": "SPHINXMIX", 90 | "link": { 91 | "from_state": "ACCEPTED", 92 | "from_endpoint_id": "mix" 93 | }, 94 | "description": "Mixing node for 'mix'", 95 | "endpoint_id": "03dbb59_for_ep_mix", 96 | "endpoint_params": "{\n}\n", 97 | "admin": "03dbb592837c9f8647063f0508b086fbf4971c9f3c59e3a412d6a45be4", 98 | "peer_id": "03dbb592837c9f8647063f0508b086fbf4971c9f3c59e3a412d6a45be4", 99 | "public": false 100 | } 101 | }, 102 | "input": { 103 | "description": "Gateway for mixnet 'mix'", 104 | "endpoint_params": "{\n}\n", 105 | "admin": "03dbb592837c9f8647063f0508b086fbf4971c9f3c59e3a412d6a45be4", 106 | "endpoint_type": "SPHINXMIX_GATEWAY", 107 | "endpoint_id": "mix", 108 | "peer_id": "0327a4eb34641189269ccd9af8b18b06fefdef0edb831ebec0f06f0ba6", 109 | "public": true 110 | }, 111 | "size_min": 100, 112 | "output": { 113 | "endpoint_type": "SPHINXMIX_OUTPUT", 114 | "link": { 115 | "from_state": "ACCEPTED", 116 | "from_endpoint_id": "021729f_for_ep_mix" 117 | }, 118 | "description": "Output of mixnet 'mix'", 119 | "endpoint_id": "mix_output", 120 | "endpoint_params": "{\n}\n", 121 | "admin": "03dbb592837c9f8647063f0508b086fbf4971c9f3c59e3a412d6a45be4", 122 | "peer_id": "0327a4eb34641189269ccd9af8b18b06fefdef0edb831ebec0f06f0ba6", 123 | "public": true 124 | } 125 | }, 126 | "title": "MIXNET_SPEC" 127 | }, 128 | "ENDPOINT_021729f_for_ep_mix": { 129 | "description": null, 130 | "value": true, 131 | "title": "ENDPOINT_021729f_for_ep_mix" 132 | }, 133 | "ENDPOINT_03dbb59_for_ep_mix": { 134 | "description": null, 135 | "value": true, 136 | "title": "ENDPOINT_03dbb59_for_ep_mix" 137 | }, 138 | "ENDPOINT_mix": { 139 | "description": null, 140 | "value": true, 141 | "title": "ENDPOINT_mix" 142 | }, 143 | "ENDPOINT_mix_output": { 144 | "description": null, 145 | "value": true, 146 | "title": "ENDPOINT_mix_output" 147 | } 148 | } -------------------------------------------------------------------------------- /test/mixing/config/client: -------------------------------------------------------------------------------- 1 | { 2 | "MIXNET_URL": { 3 | "description": null, 4 | "value": "http://127.0.0.1:8000/panoramix/endpoints/mix/", 5 | "title": "MIXNET_URL" 6 | }, 7 | "CATALOG_URL": { 8 | "description": null, 9 | "value": "http://127.0.0.1:8000/panoramix", 10 | "title": "CATALOG_URL" 11 | }, 12 | "ENDPOINT_ID": { 13 | "description": null, 14 | "value": "mix", 15 | "title": "ENDPOINT_ID" 16 | }, 17 | "MIXNET_ID": { 18 | "description": null, 19 | "value": "0327a4eb34641189269ccd9af8b18b06fefdef0edb831ebec0f06f0ba6", 20 | "title": "MIXNET_ID" 21 | }, 22 | "CRYPTO_PARAMS": { 23 | "description": null, 24 | "value": { 25 | "BODY_LEN": 1024, 26 | "GROUP": 713, 27 | "HEADER_LEN": 192 28 | }, 29 | "title": "CRYPTO_PARAMS" 30 | }, 31 | "MIXERS": { 32 | "description": null, 33 | "value": [ 34 | "03dbb592837c9f8647063f0508b086fbf4971c9f3c59e3a412d6a45be4", 35 | "021729f05f0d09548a05b7c41fe4dbea98c02873b50b1ed0d2900c9800" 36 | ], 37 | "title": "MIXERS" 38 | }, 39 | "KEY": { 40 | "description": null, 41 | "value": { 42 | "SECRET": "BD3700F7AAA7E87C6E52162FCDC6F58EBCD701D7459A3FA5A15AC4AA", 43 | "PUBLIC": "03d885928af002985d5e0c9d76472231970a02e1b0b8b69970b36a3c52" 44 | }, 45 | "title": "KEY" 46 | } 47 | } -------------------------------------------------------------------------------- /test/mixing/config/contributor: -------------------------------------------------------------------------------- 1 | { 2 | "CATALOG_URL": { 3 | "description": null, 4 | "value": "http://127.0.0.1:8000/panoramix/", 5 | "title": "CATALOG_URL" 6 | }, 7 | "CREATE_CRYPTO_PARAMS": { 8 | "description": null, 9 | "value": { 10 | "BODY_LEN": 1024, 11 | "GROUP": 713, 12 | "HEADER_LEN": 192 13 | }, 14 | "title": "CREATE_CRYPTO_PARAMS" 15 | }, 16 | "CRYPTO_PARAMS": { 17 | "description": null, 18 | "value": { 19 | "BODY_LEN": 1024, 20 | "GROUP": 713, 21 | "HEADER_LEN": 192 22 | }, 23 | "title": "CRYPTO_PARAMS" 24 | }, 25 | "KEY": { 26 | "description": null, 27 | "value": { 28 | "SECRET": "2BC21183CD50E9501784DA339A4232EB1BED542ADD8783A9293A69DE", 29 | "PUBLIC": "021729f05f0d09548a05b7c41fe4dbea98c02873b50b1ed0d2900c9800" 30 | }, 31 | "title": "KEY" 32 | }, 33 | "PEER_NAME": { 34 | "description": null, 35 | "value": "p2", 36 | "title": "PEER_NAME" 37 | }, 38 | "PEER_ID": { 39 | "description": null, 40 | "value": "021729f05f0d09548a05b7c41fe4dbea98c02873b50b1ed0d2900c9800", 41 | "title": "PEER_ID" 42 | }, 43 | "SETUP_ROLE": { 44 | "description": null, 45 | "value": "contrib", 46 | "title": "SETUP_ROLE" 47 | }, 48 | "MIXNET_NAME": { 49 | "description": null, 50 | "value": "mix", 51 | "title": "MIXNET_NAME" 52 | }, 53 | "MIXNET_PEER": { 54 | "description": null, 55 | "value": "0327a4eb34641189269ccd9af8b18b06fefdef0edb831ebec0f06f0ba6", 56 | "title": "MIXNET_PEER" 57 | }, 58 | "MIN_SIZE": { 59 | "description": null, 60 | "value": 100, 61 | "title": "MIN_SIZE" 62 | }, 63 | "MIXNET_SPEC": { 64 | "description": null, 65 | "value": { 66 | "mix_endpoints": { 67 | "021729f05f0d09548a05b7c41fe4dbea98c02873b50b1ed0d2900c9800": { 68 | "endpoint_type": "SPHINXMIX", 69 | "link": { 70 | "from_state": "ACCEPTED", 71 | "from_endpoint_id": "03dbb59_for_ep_mix" 72 | }, 73 | "description": "Mixing node for 'mix'", 74 | "endpoint_id": "021729f_for_ep_mix", 75 | "endpoint_params": "{\n}\n", 76 | "admin": "021729f05f0d09548a05b7c41fe4dbea98c02873b50b1ed0d2900c9800", 77 | "peer_id": "021729f05f0d09548a05b7c41fe4dbea98c02873b50b1ed0d2900c9800", 78 | "public": false 79 | }, 80 | "03dbb592837c9f8647063f0508b086fbf4971c9f3c59e3a412d6a45be4": { 81 | "endpoint_type": "SPHINXMIX", 82 | "link": { 83 | "from_state": "ACCEPTED", 84 | "from_endpoint_id": "mix" 85 | }, 86 | "description": "Mixing node for 'mix'", 87 | "endpoint_id": "03dbb59_for_ep_mix", 88 | "endpoint_params": "{\n}\n", 89 | "admin": "03dbb592837c9f8647063f0508b086fbf4971c9f3c59e3a412d6a45be4", 90 | "peer_id": "03dbb592837c9f8647063f0508b086fbf4971c9f3c59e3a412d6a45be4", 91 | "public": false 92 | } 93 | }, 94 | "input": { 95 | "description": "Gateway for mixnet 'mix'", 96 | "endpoint_params": "{\n}\n", 97 | "admin": "admin", 98 | "endpoint_type": "SPHINXMIX_GATEWAY", 99 | "endpoint_id": "mix", 100 | "peer_id": "0327a4eb34641189269ccd9af8b18b06fefdef0edb831ebec0f06f0ba6", 101 | "public": true 102 | }, 103 | "size_min": 100, 104 | "output": { 105 | "endpoint_type": "SPHINXMIX_OUTPUT", 106 | "link": { 107 | "from_state": "ACCEPTED", 108 | "from_endpoint_id": "021729f_for_ep_mix" 109 | }, 110 | "description": "Output of mixnet 'mix'", 111 | "endpoint_id": "mix_output", 112 | "endpoint_params": "{\n}\n", 113 | "admin": "admin", 114 | "peer_id": "0327a4eb34641189269ccd9af8b18b06fefdef0edb831ebec0f06f0ba6", 115 | "public": true 116 | } 117 | }, 118 | "title": "MIXNET_SPEC" 119 | } 120 | } -------------------------------------------------------------------------------- /test/mixing/config/panoramix_db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grnet/panoramix/a9b788e1a073e106d5542511094fe4ac3828d4b6/test/mixing/config/panoramix_db.sqlite3 -------------------------------------------------------------------------------- /test/mixing/mixing.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import time 3 | from panoramix.common import SphinxmixClient 4 | from panoramix.agent import client as end_user 5 | from panoramix.config import cfg 6 | 7 | messages = sorted([('%02d' % i, 'user%s' % i) for i in range(100)]) 8 | 9 | 10 | t_start = datetime.datetime.now() 11 | print "Sending" 12 | for message in messages: 13 | end_user.send_message(text=message[0], recipient=message[1]) 14 | print "Sent messages" 15 | 16 | t_sent = datetime.datetime.now() 17 | 18 | mixnet_url = cfg.get('MIXNET_URL').rstrip('/') 19 | endpoint_prefix, endpoint_id = mixnet_url.rsplit('/', 1) 20 | out_endpoint_id = '%s_output' % endpoint_id 21 | 22 | 23 | def init_client(): 24 | panoramix_client = SphinxmixClient() 25 | catalog_url = cfg.get("CATALOG_URL") 26 | panoramix_client.register_catalog_url(catalog_url) 27 | panoramix_client.register_crypto_client(cfg) 28 | return panoramix_client 29 | 30 | panoramix_client = init_client() 31 | 32 | while True: 33 | outbox = panoramix_client.messages_list(out_endpoint_id) 34 | if outbox: 35 | print "Found outbox, checking..." 36 | outbox_messages = [ 37 | (msg['text'], msg['recipient']) 38 | for msg in outbox 39 | ] 40 | if messages == outbox_messages: 41 | raise AssertionError("messages not shuffled") 42 | if messages != sorted(outbox_messages): 43 | raise AssertionError("messages do not match") 44 | print "Outbox verified!" 45 | break 46 | else: 47 | print "Outbox not ready yet..." 48 | time.sleep(1) 49 | 50 | t_end = datetime.datetime.now() 51 | print "Time sending", (t_sent - t_start).total_seconds() 52 | print "Time mixing", (t_end - t_sent).total_seconds() 53 | print "Time total", (t_end - t_start).total_seconds() 54 | -------------------------------------------------------------------------------- /test/mixing/run-admin.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | WORKSPACE='/tmp/panoramix_mixing_test' 5 | 6 | PANORAMIX_CONFIG=${WORKSPACE}/admin panoramix-wizard 7 | -------------------------------------------------------------------------------- /test/mixing/run-client-daemon.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | WORKSPACE='/tmp/panoramix_mixing_test' 5 | 6 | PANORAMIX_CONFIG=${WORKSPACE}/client sphinxmix-agent 7 | -------------------------------------------------------------------------------- /test/mixing/run-contrib.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | WORKSPACE='/tmp/panoramix_mixing_test' 5 | 6 | PANORAMIX_CONFIG=${WORKSPACE}/contributor panoramix-wizard 7 | -------------------------------------------------------------------------------- /test/mixing/run-service.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | WORKSPACE='/tmp/panoramix_mixing_test' 5 | 6 | export PANORAMIX_DATABASE=${WORKSPACE}/panoramix_db.sqlite3 7 | 8 | panoramix-manage migrate 9 | 10 | panoramix-manage runserver --nothreading 127.0.0.1:8000 11 | -------------------------------------------------------------------------------- /test/mixing/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | WORKSPACE='/tmp/panoramix_mixing_test' 5 | 6 | cd "$(dirname "$0")" 7 | rm -rf ${WORKSPACE} 8 | cp -r config ${WORKSPACE} 9 | 10 | ./run-service.sh & 11 | sleep 10 12 | 13 | ./run-admin.sh & 14 | ./run-contrib.sh & 15 | ./run-client-daemon.sh & 16 | sleep 3 17 | 18 | PANORAMIX_CONFIG=${WORKSPACE}/client python mixing.py 19 | 20 | echo Killing daemons... 21 | pkill -f panoramix-wizard 22 | pkill -f sphinxmix-agent 23 | pkill -f 'panoramix-manage runserver' 24 | -------------------------------------------------------------------------------- /test/mixing/shell.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | WORKSPACE='/tmp/panoramix_mixing_test' 5 | 6 | export PANORAMIX_DATABASE=${WORKSPACE}/panoramix_db.sqlite3 7 | 8 | PANORAMIX_CONFIG=${WORKSPACE}/server panoramix-manage shell 9 | -------------------------------------------------------------------------------- /trustpanel/requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | flask-cors 3 | requests 4 | consensus-client 5 | specular 6 | -------------------------------------------------------------------------------- /trustpanel/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | CURPATH = os.path.dirname(os.path.realpath(__file__)) 5 | 6 | def get_requirements(): 7 | req_file = os.path.join(CURPATH, "requirements.txt") 8 | with open(req_file) as f: 9 | return [ 10 | x.strip('\n') 11 | for x in f.readlines() 12 | if x and x[0] != '#' 13 | ] 14 | 15 | version_file = os.path.join(CURPATH, "..", "version.txt") 16 | with open(version_file) as f: 17 | version = f.read().strip() 18 | 19 | package_name = 'trustpanel' 20 | description = 'Panoramix Trust Control Panel' 21 | 22 | setup( 23 | name=package_name, 24 | version=version, 25 | license='Affero GPL v3', 26 | author='GRNET S.A.', 27 | author_email='panoramix@dev.grnet.gr', 28 | description=description, 29 | packages=find_packages(), 30 | install_requires=get_requirements() 31 | ) 32 | -------------------------------------------------------------------------------- /trustpanel/trustpanel/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grnet/panoramix/a9b788e1a073e106d5542511094fe4ac3828d4b6/trustpanel/trustpanel/__init__.py -------------------------------------------------------------------------------- /ui/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.hbs] 17 | insert_final_newline = false 18 | 19 | [*.{diff,md}] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /ui/.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /ui/.eslintignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | 12 | # misc 13 | /coverage/ 14 | !.* 15 | 16 | # ember-try 17 | /.node_modules.ember-try/ 18 | /bower.json.ember-try 19 | /package.json.ember-try 20 | -------------------------------------------------------------------------------- /ui/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: 'babel-eslint', 4 | parserOptions: { 5 | ecmaVersion: 2017, 6 | sourceType: 'module' 7 | }, 8 | plugins: [ 9 | 'ember' 10 | ], 11 | extends: [ 12 | 'eslint:recommended', 13 | 'plugin:ember/recommended' 14 | ], 15 | env: { 16 | browser: true 17 | }, 18 | rules: { 19 | }, 20 | overrides: [ 21 | // node files 22 | { 23 | files: [ 24 | '.template-lintrc.js', 25 | 'ember-cli-build.js', 26 | 'testem.js', 27 | 'blueprints/*/index.js', 28 | 'config/**/*.js', 29 | 'lib/*/index.js' 30 | ], 31 | parserOptions: { 32 | sourceType: 'script', 33 | ecmaVersion: 2015 34 | }, 35 | env: { 36 | browser: false, 37 | node: true 38 | } 39 | } 40 | ] 41 | }; 42 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist/ 5 | /tmp/ 6 | 7 | # dependencies 8 | /bower_components/ 9 | /node_modules/ 10 | 11 | # misc 12 | /.sass-cache 13 | /connect.lock 14 | /coverage/ 15 | /libpeerconnection.log 16 | /npm-debug.log* 17 | /testem.log 18 | /yarn-error.log 19 | 20 | # ember-try 21 | /.node_modules.ember-try/ 22 | /bower.json.ember-try 23 | /package.json.ember-try 24 | -------------------------------------------------------------------------------- /ui/.template-lintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'recommended' 5 | }; 6 | -------------------------------------------------------------------------------- /ui/.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | - "6" 5 | 6 | sudo: false 7 | dist: trusty 8 | 9 | addons: 10 | chrome: stable 11 | 12 | cache: 13 | directories: 14 | - $HOME/.npm 15 | 16 | env: 17 | global: 18 | # See https://git.io/vdao3 for details. 19 | - JOBS=1 20 | 21 | before_install: 22 | - npm config set spin false 23 | 24 | script: 25 | - npm run lint:hbs 26 | - npm run lint:js 27 | - npm test 28 | -------------------------------------------------------------------------------- /ui/.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | # zeus 2 | 3 | This README outlines the details of collaborating on this Ember application. 4 | A short introduction of this app could easily go here. 5 | 6 | ## Prerequisites 7 | 8 | You will need the following things properly installed on your computer. 9 | 10 | * [Git](https://git-scm.com/) 11 | * [Node.js](https://nodejs.org/) (with npm) 12 | * [Ember CLI](https://ember-cli.com/) 13 | * [Google Chrome](https://google.com/chrome/) 14 | 15 | ## Installation 16 | 17 | * `git clone ` this repository 18 | * `cd zeus` 19 | * `npm install` 20 | 21 | ## Running / Development 22 | 23 | * `ember serve` 24 | * Visit your app at [http://localhost:4200](http://localhost:4200). 25 | * Visit your tests at [http://localhost:4200/tests](http://localhost:4200/tests). 26 | 27 | ### Code Generators 28 | 29 | Make use of the many generators for code, try `ember help generate` for more details 30 | 31 | ### Running Tests 32 | 33 | * `ember test` 34 | * `ember test --server` 35 | 36 | ### Linting 37 | 38 | * `npm run lint:hbs` 39 | * `npm run lint:js` 40 | * `npm run lint:js -- --fix` 41 | 42 | ### Building 43 | 44 | * `ember build` (development) 45 | * `ember build --environment production` (production) 46 | 47 | ### Deploying 48 | 49 | Specify what it takes to deploy your app. 50 | 51 | ## Further Reading / Useful Links 52 | 53 | * [ember.js](https://emberjs.com/) 54 | * [ember-cli](https://ember-cli.com/) 55 | * Development Browser Extensions 56 | * [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi) 57 | * [ember inspector for firefox](https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/) 58 | -------------------------------------------------------------------------------- /ui/app/app.js: -------------------------------------------------------------------------------- 1 | import Application from '@ember/application'; 2 | import Resolver from './resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from './config/environment'; 5 | 6 | const App = Application.extend({ 7 | modulePrefix: config.modulePrefix, 8 | podModulePrefix: config.podModulePrefix, 9 | Resolver 10 | }); 11 | 12 | loadInitializers(App, config.modulePrefix); 13 | 14 | export default App; 15 | -------------------------------------------------------------------------------- /ui/app/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grnet/panoramix/a9b788e1a073e106d5542511094fe4ac3828d4b6/ui/app/components/.gitkeep -------------------------------------------------------------------------------- /ui/app/components/election-view/component.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import { argument } from '@ember-decorators/argument'; 3 | import { computed } from '@ember-decorators/object'; 4 | import { A } from '@ember/array'; 5 | 6 | 7 | const allPaths = A([]); 8 | allPaths.includes = function() { return true; } 9 | 10 | export default class ElectionViewComponent extends Component { 11 | @argument stages; 12 | @argument user; 13 | @argument expandedPaths; 14 | @argument actionDisabled; 15 | @argument('action') docAction; 16 | @argument('action') onValueChange; 17 | @argument('action') onKeyLock; 18 | @argument('action') expandPath; 19 | @argument('action') collapsePath; 20 | @argument('action') showStatus; 21 | 22 | allPaths = allPaths; 23 | } 24 | -------------------------------------------------------------------------------- /ui/app/components/election-view/template.hbs: -------------------------------------------------------------------------------- 1 | {{#each-in stages as |id stage|}} 2 | {{stage-view 3 | stage=stage 4 | meta=(get stage.meta stage.path) 5 | expandedPaths=allPaths 6 | tagName='' 7 | expanded=(if stage.running true (contains stage.path expandedPaths)) 8 | expandPath=expandPath 9 | collapsePath=collapsePath 10 | onValueChange=onValueChange 11 | onKeyLock=onKeyLock 12 | showStatus=(action showStatus stage.id) 13 | actionDisabled=actionDisabled 14 | docAction=docAction}} 15 | {{/each-in}} 16 | -------------------------------------------------------------------------------- /ui/app/components/paper-item-value/component.js: -------------------------------------------------------------------------------- 1 | import Component from 'apimas-docs/components/doc-item-value/component'; 2 | import { layout } from '@ember-decorators/component'; 3 | import template from './template'; 4 | 5 | @layout(template) 6 | export default class PaperItemValueComponent extends Component { 7 | } 8 | -------------------------------------------------------------------------------- /ui/app/components/paper-item-value/template.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{#if (eq meta.action "choices") }} 3 | {{#paper-select 4 | selected=value 5 | onChange=onChange 6 | options=choices 7 | placeholder=(if meta.placeholder meta.placeholder label) 8 | class="flex-grow" 9 | as |choice| 10 | }} 11 | {{ choice }} 12 | {{/paper-select}} 13 | {{/if}} 14 | {{#if (eq meta.action "compute_element") }} 15 | 16 | {{#paper-button 17 | disabled=(or meta.action_disabled meta.dict_locked) 18 | class="compute" 19 | primary=true 20 | raised=true 21 | onClick=(action "onButtonClick") 22 | }} 23 |
24 | {{paper-icon (if meta.icon meta.icon "add") size=12 }}{{ meta.label }} 25 |
26 | {{/paper-button}} 27 | {{/if}} 28 | {{#if meta.lock_action }} 29 | {{#paper-button 30 | disabled=meta.dict_locked 31 | class="locked-dict" 32 | primary=true 33 | raised=true 34 | onClick=(action "onLockClick") 35 | }} 36 |
37 | {{paper-icon (if meta.icon meta.icon "lock") size=12 }}lock 38 |
39 | {{/paper-button}} 40 | {{/if}} 41 | 42 | 43 | {{#if 44 | (and 45 | (not (eq meta.mode "auto")) 46 | (not meta.action) 47 | (not (eq meta.type "file")) 48 | ) 49 | }} 50 | {{#if (eq meta.type "datetime")}} 51 | {{ember-flatpickr 52 | allowInput=true 53 | appendDataInput=true 54 | classNames=classString 55 | date=(readonly textValue) 56 | enableTime=true 57 | minDate=minDate 58 | dateFormat="d/m/Y H:i" 59 | defaultHour=00 60 | defaultMinute=00 61 | time_24hr=true 62 | focusOut=(action "updateValue") 63 | onClose=(action "updateDateValue") 64 | onChange=(queue (action (mut textValue))) 65 | placeholder="Pick date" 66 | }} 67 | {{/if}} 68 | {{#if 69 | (and 70 | (or (eq meta.type "string") (eq meta.type "int")) 71 | ) 72 | }} 73 | {{paper-input 74 | class="flex" 75 | type="string" 76 | placeholder=(if meta.placeholder meta.placeholder "") 77 | value=textValue 78 | focusOut=(action "updateValue") 79 | focused=focused 80 | onChange=(queue (action (mut textValue))) 81 | }} 82 | {{/if}} 83 | {{else}} 84 | {{#if (not (eq meta.action "choices"))}} 85 | {{#if (eq meta.type "file")}} 86 | {{ jsonValue.hash }} 87 | {{/if}} 88 | {{#if (eq meta.type "string")}} 89 | {{ value }} 90 | {{/if}} 91 | {{#if (eq meta.type "int")}} 92 | {{ value }} 93 | {{/if}} 94 | {{/if}} 95 | {{/if}} 96 | {{yield}} 97 |
98 | -------------------------------------------------------------------------------- /ui/app/components/stage-key-view/component.js: -------------------------------------------------------------------------------- 1 | import Component from 'apimas-docs/components/doc-view-item/component'; 2 | import { argument } from '@ember-decorators/argument'; 3 | import { classNames, tagName, layout, attribute } from '@ember-decorators/component'; 4 | import { action, computed } from '@ember-decorators/object'; 5 | import template from './template'; 6 | 7 | @tagName('') 8 | @layout(template) 9 | export default class StageKeyViewItemComponent extends Component { 10 | 11 | constructor() { 12 | super(...arguments); 13 | this.classNameBindings = []; 14 | this.attributeBindings = []; 15 | } 16 | 17 | @attribute role = "listitem"; 18 | @attribute tabindex = "-1"; 19 | 20 | @argument root; 21 | @argument path; 22 | @argument key; 23 | @argument doc; 24 | @argument completed; 25 | @argument analysis; 26 | @argument disabled; 27 | @argument isDoc; 28 | @argument depth; 29 | @argument expanded; 30 | @argument meta; 31 | @argument type; 32 | 33 | @argument('action') expandPath = function() {}; 34 | @argument('action') collapsePath = function() {}; 35 | @argument('action') onClick = function() {}; 36 | @argument('action') onChange = function() {}; 37 | @argument('action') onLock = function() {}; 38 | @argument('action') showStatus = function() {}; 39 | 40 | @computed('key', 'meta.key_label') 41 | get label() { 42 | let dflt = Ember.String.capitalize(this.key.split('_').join(' ')); 43 | return this.meta && this.meta.key_label || dflt; 44 | } 45 | 46 | @computed('meta.state_label') 47 | get stateCls() { 48 | let cls = this.meta && this.meta.state_label; 49 | return `state state-${cls}`; 50 | } 51 | 52 | // this is to decide if raw document value should be displayed 53 | // when node stage is closed ('done' state) 54 | @computed('meta.type', 'doc') 55 | get rawDocView() { 56 | return !['file'].includes(this.meta.type) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /ui/app/components/stage-key-view/template.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{#if (and (not isDoc) meta.positions) }}
{{/if}} 3 |
4 |
5 | {{label}}{{#if meta.dict_locked }}{{paper-icon "lock" class="locked"}}{{/if}} 6 | {{# if meta.help_text }} 7 | {{ meta.help_text }} 8 | {{/if}} 9 |
10 |
11 | {{#if (and (eq meta.state_label 'done') rawDocView) }} 12 | {{if doc (if (not isDoc) doc) 'Not set'}} 13 | {{else}} 14 | {{paper-item-value classNames="layout-column" label=label value=doc meta=meta onChange=onChange onLock=onLock}} 15 | {{/if}} 16 |
17 |
18 |
19 | {{#if isDoc}} 20 |
21 | {{ yield }} 22 |
23 | {{/if}} 24 | -------------------------------------------------------------------------------- /ui/app/components/stage-view/component.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import { argument } from '@ember-decorators/argument'; 3 | import { computed } from '@ember-decorators/object'; 4 | import { classNames, className } from '@ember-decorators/component'; 5 | import { capitalize } from '@ember/string'; 6 | 7 | @classNames('stage') 8 | export default class StageViewComponent extends Component { 9 | @argument meta; 10 | @argument stage; 11 | @argument expanded; 12 | @argument expandedPaths; 13 | @argument actionDisabled; 14 | @argument('action') docAction; 15 | @argument('action') onValueChange; 16 | @argument('action') onKeyLock; 17 | @argument('action') expandPath; 18 | @argument('action') collapsePath; 19 | @argument('action') showStatus; 20 | 21 | actionableLabels = ['FINISH', 'PROPOSE', 'TRANSITION', 'ENHANCE', 'CONSENT', 'CONFLICT'] 22 | actionLabelMap = { 23 | 'ENHANCE': 'CONSENT & PROPOSE', 24 | 'NO_TRANSITION': 'PROPOSE', 25 | 'AUTOCLOSE': 'WAIT' 26 | } 27 | 28 | @computed('meta.state_label') 29 | get actionCls() { 30 | let state_label = this.meta.state_label; 31 | if (!state_label) { return; } 32 | return 'to-state-' + state_label.toLowerCase(); 33 | } 34 | 35 | @computed('meta.state_label') 36 | get actionLabel() { 37 | let state_label = this.meta.state_label; 38 | let label = this.actionLabelMap[(state_label || '').toUpperCase()] || state_label && state_label.split('_').join(' ') || 'WAIT'; 39 | return capitalize(label); 40 | } 41 | 42 | @computed('meta.state_label') 43 | get canAction() { 44 | let label = this.meta.state_label; 45 | return this.actionableLabels.includes(label && label.toUpperCase()); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ui/app/components/stage-view/template.hbs: -------------------------------------------------------------------------------- 1 | {{#if (not stage.pending) }} 2 | {{#paper-expansion-panel 3 | class=(concat 4 | (if (and stage.running (not meta.state_label)) "state-undefined " "") 5 | (if meta.state_label 6 | (concat "state-" meta.state_label " ") 7 | ) 8 | " stage" 9 | ) 10 | expanded=expanded 11 | onExpandedChange=(action expandPath stage.path) 12 | as |panel| 13 | }} 14 | {{#panel.collapsed}} 15 |
16 |
17 |
{{meta.title}}
18 |
{{meta.description}}
19 | {{paper-icon "keyboard_arrow_down"}} 20 | {{#if stage.completed}} 21 | {{paper-icon "lock" class="lock"}} 22 | {{else}} 23 | {{paper-icon "lock_open" class="unlock"}} 24 | {{/if}} 25 |
26 |
27 | {{#if stage.negotiation}} 28 |
negotiation{{stage.negotiation}}
29 | {{/if}} 30 | {{#if stage.consensus_id }} 31 |
consensus{{stage.consensus_id}}
32 | {{/if}} 33 |
34 |
35 | {{/panel.collapsed}} 36 | {{#panel.expanded as |expanded|}} 37 | {{#expanded.header}} 38 |
39 |
40 |
{{meta.title}}
41 |
{{meta.description}}
42 | {{paper-icon "keyboard_arrow_down"}} 43 | {{#if stage.completed}} 44 | {{paper-icon "lock" class="lock"}} 45 | {{else}} 46 | {{paper-icon "lock_open" class="unlock"}} 47 | {{/if}} 48 |
49 |
50 | {{#if stage.negotiation}} 51 |
negotiation{{stage.negotiation}}
52 | {{/if}} 53 | {{#if stage.consensus_id }} 54 |
consensus{{stage.consensus_id}}
55 | {{/if}} 56 |
57 |
58 | {{/expanded.header}} 59 | {{#expanded.content class="stage"}} 60 | {{doc-view 61 | root="" 62 | key=stage.id 63 | doc=stage.document 64 | meta=stage.meta 65 | tagName="div" 66 | docTagName="md-list" 67 | itemTagName="" 68 | expandedPaths=expandedPaths 69 | itemComponent=(component "stage-key-view" showStatus=showStatus) 70 | onValueChange=(action onValueChange stage.id) 71 | onKeyLock=(action onKeyLock stage.id) 72 | }} 73 | {{/expanded.content}} 74 | {{#if (not stage.completed) }} 75 | {{#expanded.footer}} 76 | {{#if (gt stage.instance 1)}} 77 | RUN{{ stage.instance }} 78 | {{/if}} 79 | 80 | {{#if (not stage.completed)}} 81 | {{#if canAction }} 82 | {{#paper-button 83 | class=actionCls 84 | disabled=(or actionDisabled (not canAction)) 85 | primary=true 86 | raised=true 87 | onClick=(action docAction stage.id) 88 | }} 89 | {{ actionLabel }} 90 | {{/paper-button}} 91 | {{else}} 92 | {{ actionLabel }} 93 | {{/if}} 94 | {{/if}} 95 | {{/expanded.footer}} 96 | {{/if}} 97 | {{/panel.expanded}} 98 | {{/paper-expansion-panel}} 99 | {{else}} 100 | 101 | {{#paper-item}} 102 |
{{meta.title}}
103 |
{{meta.description}}
104 | {{/paper-item}} 105 |
106 | {{/if}} 107 | -------------------------------------------------------------------------------- /ui/app/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grnet/panoramix/a9b788e1a073e106d5542511094fe4ac3828d4b6/ui/app/controllers/.gitkeep -------------------------------------------------------------------------------- /ui/app/controllers/application.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | 3 | export default class ApplicationController extends Controller { 4 | } 5 | -------------------------------------------------------------------------------- /ui/app/controllers/election.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { action, computed } from '@ember-decorators/object'; 3 | import { service } from '@ember-decorators/service'; 4 | import { A } from '@ember/array'; 5 | import { typeOf } from '@ember/utils'; 6 | import { set, get } from '@ember/object'; 7 | import { task, timeout } from 'ember-concurrency'; 8 | import ENV from 'zeus/config/environment'; 9 | 10 | 11 | export default class ElectionController extends Controller { 12 | 13 | updateInProgress = false; 14 | 15 | init() { 16 | super.init(...arguments); 17 | this.manual = false; 18 | this.set('queryParams', ['manual']); 19 | } 20 | 21 | @service api; 22 | @service paperToaster; 23 | 24 | @computed('manual') 25 | get manualRefresh() { 26 | return this.manual 27 | } 28 | set manualRefresh(val) { return val; } 29 | 30 | appTitle = ENV.APP.title; 31 | appSubtitle = ENV.APP.subtitle; 32 | 33 | inProgress = true; 34 | updateInProgress = false; 35 | expandedPaths = []; 36 | 37 | handleApiError(err) { 38 | console.error(err); 39 | this.paperToaster.show("api error", { 40 | duration: 3000, 41 | toastClass: 'md-warn' 42 | }); 43 | return null; 44 | } 45 | 46 | updateRunningPaths(paths) { 47 | let model = this.model; 48 | Object.keys(model).forEach((user) => { 49 | let _model = model[user]; 50 | Object.keys(_model).forEach((key) => { 51 | if (_model[key].running) { 52 | paths.addObject(_model[key].path); 53 | } 54 | }); 55 | }); 56 | this.expandedPaths.setObjects(paths); 57 | } 58 | 59 | @action onKeyLock(user, stageId, item, value, evt) { 60 | this.onValueChange(user, stageId, item, "CLOSE", evt); 61 | } 62 | 63 | @action onValueChange(user, stageId, item, value, evt) { 64 | evt && evt.stopPropagation && evt.stopPropagation(); 65 | 66 | console.log("UPDATE VALUE", item, value); 67 | let model = this.model[user]; 68 | let path = item.path; 69 | let modelPath = [stageId, 'document'] .concat(item.path.split('/').slice(2)).join('.'); 70 | let modelDotsPath = modelPath.split('/').join('.'); 71 | let meta = model[stageId].meta[path]; 72 | 73 | let update = this.get('updateModel').perform(); 74 | let error = this.handleApiError.bind(this); 75 | 76 | let context = get(model, modelPath) || {}; 77 | let key = path.split('/').slice(2).join('/') 78 | 79 | let promise = this.api.updatePath(user, stageId, key, value, meta, model[stageId]); 80 | this.set('updateInProgress', true); 81 | return promise.then(update).catch(error).finally(() => { 82 | this.set('updateInProgress', false); 83 | }); 84 | } 85 | 86 | @action docAction(user, stage) { 87 | let error = this.handleApiError.bind(this); 88 | let update = this.get('updateModel'); 89 | let model = this.model[user][stage]; 90 | let completed = model.completed; 91 | this.api.contribute(user, stage, model).then((resp) => { 92 | update.perform().then((resp) => { 93 | if (model.completed != completed) { 94 | update.perform().then(() => { 95 | let paths = this.expandedPaths.concat(); 96 | paths.removeObject(model.path); 97 | this.updateRunningPaths(paths); 98 | }).catch(() => { return true }); 99 | } else { 100 | return resp; 101 | } 102 | }).catch(() => { return true }); 103 | }).catch(error); 104 | } 105 | 106 | @action reloadModel() { 107 | this.get('updateModel').perform(); 108 | } 109 | 110 | @action expandPath(path, state=null) { 111 | if (state != null) { 112 | if (state) { 113 | return this.expandPath(path); 114 | } else { 115 | return this.collapsePath(path); 116 | } 117 | } 118 | if (!this.expandedPaths.includes(path)) { 119 | this.expandedPaths.addObject(path); 120 | } 121 | } 122 | 123 | @action collapsePath(path) { 124 | if (this.expandedPaths.includes(path)) { 125 | this.expandedPaths.removeObject(path); 126 | } 127 | } 128 | 129 | @action showStatus(user, stage, path, label) { 130 | let meta = this.model[user][stage].meta[path]; 131 | let data = { 132 | path: label || path, 133 | meta: this.model[user][stage].meta[path] 134 | }; 135 | set(this, 'analysis', data); 136 | set(this, 'showDialog', true); 137 | } 138 | 139 | @action hideStatus(user, path) { 140 | set(this, 'analysis', null); 141 | set(this, 'showDialog', false); 142 | } 143 | 144 | onRefreshError = function() { 145 | debugger; 146 | }.on('refreshModels:errored') 147 | 148 | refreshModels = task(function * () { 149 | while (!this.manualRefresh) { 150 | yield timeout(ENV.APP.modelInterval || 5000); 151 | this.get('updateModel').perform().catch((err) => { 152 | console.log("refresh error", err); 153 | }); 154 | } 155 | }).evented(); 156 | 157 | updateModel = task(function * () { 158 | for (let user of this.users) { 159 | if (!this.model || !this.model[user]) { continue; } 160 | try { 161 | yield this.api.updateStages(this.model[user], user, this.negId); 162 | } catch(err) { 163 | this.handleApiError(err); 164 | } 165 | } 166 | }).drop(); 167 | } 168 | -------------------------------------------------------------------------------- /ui/app/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grnet/panoramix/a9b788e1a073e106d5542511094fe4ac3828d4b6/ui/app/helpers/.gitkeep -------------------------------------------------------------------------------- /ui/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Panoramix Trust Control Panel 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | 12 | 13 | 14 | 15 | {{content-for "head-footer"}} 16 | 17 | 18 | {{content-for "body"}} 19 | 20 | 21 | 22 | 23 | {{content-for "body-footer"}} 24 | 25 | 26 | -------------------------------------------------------------------------------- /ui/app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grnet/panoramix/a9b788e1a073e106d5542511094fe4ac3828d4b6/ui/app/models/.gitkeep -------------------------------------------------------------------------------- /ui/app/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from 'ember-resolver'; 2 | 3 | export default Resolver; 4 | -------------------------------------------------------------------------------- /ui/app/router.js: -------------------------------------------------------------------------------- 1 | import EmberRouter from '@ember/routing/router'; 2 | import config from './config/environment'; 3 | 4 | const Router = EmberRouter.extend({ 5 | location: config.locationType, 6 | rootURL: config.rootURL 7 | }); 8 | 9 | Router.map(function() { 10 | this.route('election-with-id', { path: '/:user/*path' }) 11 | this.route('election', { path: '/:user' }) 12 | }); 13 | 14 | export default Router; 15 | -------------------------------------------------------------------------------- /ui/app/routes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grnet/panoramix/a9b788e1a073e106d5542511094fe4ac3828d4b6/ui/app/routes/.gitkeep -------------------------------------------------------------------------------- /ui/app/routes/election-with-id.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | 3 | export default Route.extend({ 4 | redirect(transition) { 5 | let user = this.paramsFor('election_with_id').user || 'trustee1'; 6 | this.transitionTo('election', user); 7 | } 8 | }); 9 | 10 | -------------------------------------------------------------------------------- /ui/app/routes/election.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import { service } from '@ember-decorators/service'; 3 | import { set } from '@ember/object'; 4 | import { hash } from 'rsvp'; 5 | 6 | export default class ElectionRoute extends Route { 7 | @service api; 8 | 9 | init() { 10 | super.init(...arguments); 11 | this.set('queryParams', { manual: {} }); 12 | } 13 | 14 | model({user}) { 15 | this.users = user.split(','); 16 | this.stages = {}; 17 | this.negId = null; 18 | let promises = {}; 19 | 20 | this.users.forEach((user) => { 21 | promises[user] = this.api.initStages(user, this.negId); 22 | }); 23 | 24 | return hash(promises).then((models) => { 25 | let sorted = {}; 26 | this.users.forEach((user) => { 27 | sorted[user] = models[user]; 28 | }); 29 | return sorted; 30 | }); 31 | } 32 | 33 | setupController(controller, model) { 34 | super.setupController(...arguments); 35 | set(controller, 'users', this.users); 36 | set(controller, 'negId', this.negId); 37 | let key = Object.keys(model)[0] 38 | set(controller, 'appSubtitle', model[key].stage_A.global_negotiation); 39 | 40 | let runningPaths = []; 41 | controller.updateRunningPaths(runningPaths); 42 | window.$E = controller; 43 | window.model = model; 44 | } 45 | 46 | activate() { 47 | this.controllerFor('election').get('refreshModels').perform(); 48 | } 49 | 50 | deactivate() { 51 | this.controllerFor('election').get('refreshModels').stop(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ui/app/routes/index.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | 3 | export default Route.extend({ 4 | redirect(transition) { 5 | this.transitionTo('election', 'trustee1'); 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /ui/app/styles/app.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grnet/panoramix/a9b788e1a073e106d5542511094fe4ac3828d4b6/ui/app/styles/app.css -------------------------------------------------------------------------------- /ui/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 | {{outlet}} 2 | -------------------------------------------------------------------------------- /ui/app/templates/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grnet/panoramix/a9b788e1a073e106d5542511094fe4ac3828d4b6/ui/app/templates/components/.gitkeep -------------------------------------------------------------------------------- /ui/app/templates/components/election-tree-view.hbs: -------------------------------------------------------------------------------- 1 |
2 |
EXPAND
3 |
COLLAPSE
4 |
RELOAD
5 |
{{ model.meta.root.stage }}
6 |
{{ model.meta.root.label }}
7 |
8 |
9 | {{doc-view 10 | tagName='div' 11 | itemTagName='div' 12 | docTagName='div' 13 | doc=model.stages 14 | meta=model.meta 15 | analysis=model.analysis 16 | expandedPaths=expandedPaths 17 | onExpand=(action 'expandPath') 18 | onCollapse=(action 'collapsePath') 19 | onItemClick=(action 'itemClick') 20 | onValueChange=(action 'onValueChange') 21 | }} 22 |
23 | -------------------------------------------------------------------------------- /ui/app/templates/election.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{#paper-toolbar class="main" as |toolbar|}} 3 | {{#toolbar.tools}} 4 |
5 |

{{ appTitle }}

6 |

7 |

8 | mode:Zeus Election 9 |
10 |
11 | negotiation:{{ appSubtitle }} 12 |
13 |

14 |
15 | 16 | {{#if manual}} 17 | {{#if (not refreshModels.isRunning) }} 18 | {{#paper-button onClick=(action "reloadModel") iconButton=true}}{{paper-icon "refresh"}}{{/paper-button}} 19 | {{#paper-button 20 | iconButton=true 21 | onClick=(queue 22 | (action (mut manualRefresh) false) 23 | (perform refreshModels)) 24 | }} 25 | {{paper-icon "play_arrow"}} 26 | {{/paper-button}} 27 | {{else}} 28 | {{#paper-button 29 | iconButton=true 30 | warn=true 31 | onClick=(action (mut manualRefresh) true) 32 | }} 33 | {{paper-icon "stop"}} 34 | {{/paper-button}} 35 | {{/if}} 36 | {{else}} 37 | {{#if (not refreshModels.isRunning) }} 38 | {{#paper-button 39 | iconButton=true 40 | onClick=(queue 41 | (action (mut manualRefresh) false) 42 | (perform refreshModels)) 43 | }} 44 | {{paper-icon "play_arrow"}} 45 | {{/paper-button}} 46 | {{/if}} 47 | {{/if}} 48 | {{/toolbar.tools}} 49 | {{/paper-toolbar}} 50 | 51 |
52 | {{#each-in model as |user stages|}} 53 |
54 | {{#if (gt users.length 1) }} 55 |

{{ user }}

56 | {{/if}} 57 | {{election-view 58 | stages=stages 59 | expandPath=(action 'expandPath') 60 | expandedPaths=expandedPaths 61 | collapsePath=(action 'collapsePath') 62 | docAction=(action 'docAction' user) 63 | onValueChange=(action 'onValueChange' user) 64 | onKeyLock=(action 'onKeyLock' user) 65 | showStatus=(action 'showStatus' user) 66 | actionDisabled=updateInProgress 67 | }} 68 |
69 | {{/each-in}} 70 |
71 | 72 | {{#if showDialog}} 73 | {{#paper-dialog 74 | class="flex-gt-xs-50" 75 | onClose=(action "hideStatus" "cancel") 76 | origin=dialogOrigin 77 | clickOutsideToClose=true 78 | }} 79 | {{#paper-toolbar class=(concat "state-" analysis.meta.label) }} 80 | {{#paper-toolbar-tools}} 81 |

{{ analysis.path }}

82 | 83 | {{#paper-button iconButton=true onClick=(action "hideStatus" "cancel")}}{{paper-icon icon="close"}}{{/paper-button}} 84 | {{/paper-toolbar-tools}} 85 | {{/paper-toolbar}} 86 | 87 | {{#paper-dialog-content class="positions"}} 88 | 117 | {{/paper-dialog-content}} 118 | {{/paper-dialog}} 119 | {{/if}} 120 | 121 | {{paper-toaster parent="#app"}} 122 |
123 | -------------------------------------------------------------------------------- /ui/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zeustmp", 3 | "dependencies": { 4 | "ember-cli-shims": "~0.1.1" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ui/config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let window = global ? global.window || {} : window || {}; 4 | module.exports = function(environment) { 5 | let ENV = { 6 | modulePrefix: 'zeus', 7 | environment, 8 | rootURL: '/ui/', 9 | locationType: 'auto', 10 | EmberENV: { 11 | FEATURES: { 12 | // Here you can enable experimental features on an ember canary build 13 | // e.g. 'with-controller': true 14 | }, 15 | EXTEND_PROTOTYPES: { 16 | // Prevent Ember Data from overriding Date.parse. 17 | Date: false 18 | } 19 | }, 20 | 21 | APP: { 22 | apiHost: window.API_HOST || '', 23 | title: 'Panoramix Trust Control Panel', 24 | subtitle: '' 25 | } 26 | }; 27 | 28 | if (environment === 'development') { 29 | // ENV.APP.LOG_RESOLVER = true; 30 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 31 | // ENV.APP.LOG_TRANSITIONS = true; 32 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 33 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 34 | } 35 | 36 | if (environment === 'test') { 37 | // Testem prefers this... 38 | ENV.locationType = 'none'; 39 | 40 | // keep test console output quieter 41 | ENV.APP.LOG_ACTIVE_GENERATION = false; 42 | ENV.APP.LOG_VIEW_LOOKUPS = false; 43 | 44 | ENV.APP.rootElement = '#ember-testing'; 45 | ENV.APP.autoboot = false; 46 | } 47 | 48 | if (environment === 'production') { 49 | // here you can enable a production-specific feature 50 | } 51 | 52 | return ENV; 53 | }; 54 | -------------------------------------------------------------------------------- /ui/config/optional-features.json: -------------------------------------------------------------------------------- 1 | { 2 | "jquery-integration": true 3 | } 4 | -------------------------------------------------------------------------------- /ui/config/targets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const browsers = [ 4 | 'last 1 Chrome versions', 5 | 'last 1 Firefox versions', 6 | 'last 1 Safari versions' 7 | ]; 8 | 9 | const isCI = !!process.env.CI; 10 | const isProduction = process.env.EMBER_ENV === 'production'; 11 | 12 | if (isCI || isProduction) { 13 | browsers.push('ie 11'); 14 | } 15 | 16 | module.exports = { 17 | browsers 18 | }; 19 | -------------------------------------------------------------------------------- /ui/ember-cli-build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EmberApp = require('ember-cli/lib/broccoli/ember-app'); 4 | 5 | module.exports = function(defaults) { 6 | let app = new EmberApp(defaults, { 7 | // Add options here 8 | }); 9 | 10 | // Use `app.import` to add additional libraries to the generated 11 | // output files. 12 | // 13 | // If you need to use different assets in different 14 | // environments, specify an object as the first parameter. That 15 | // object's keys should be the environment name and the values 16 | // should be the asset to use in that environment. 17 | // 18 | // If the library that you are including contains AMD or ES6 19 | // modules that you would like to import into your application 20 | // please specify an object with the list of modules as keys 21 | // along with the exports of each module as its value. 22 | 23 | return app.toTree(); 24 | }; 25 | -------------------------------------------------------------------------------- /ui/lib/apimas-docs/addon/components/doc-item-value/component.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import template from './template'; 3 | import { layout, attribute, classNames } from '@ember-decorators/component'; 4 | import { action, computed, observes } from '@ember-decorators/object'; 5 | 6 | @layout(template) 7 | @classNames('doc-item-value') 8 | export default class DocItemValueComponent extends Component { 9 | @attribute 10 | @computed('meta.action') 11 | get contenteditable() { 12 | return this.meta && this.meta.action == 'edit' 13 | }; 14 | 15 | @computed('value') 16 | get jsonValue() { 17 | return JSON.parse(this.value); 18 | } 19 | 20 | @computed('meta.choices') 21 | get choices() { 22 | if (!this.meta.choices) { return []; } 23 | return this.meta.choices; 24 | } 25 | 26 | didInsertElement() { 27 | super.didInsertElement(...arguments); 28 | } 29 | 30 | @action onButtonClick(evt) { 31 | evt.stopPropagation(); 32 | this.onChange(); 33 | } 34 | 35 | @action onLockClick(evt) { 36 | evt.stopPropagation(); 37 | this.onLock(); 38 | } 39 | 40 | didReceiveAttrs() { 41 | this.set('textValue', this.value); 42 | } 43 | 44 | @observes('value') 45 | updateTextValue() { 46 | this.set('textValue', this.value); 47 | } 48 | 49 | 50 | @action updateDateValue(dates, val) { 51 | this.onChange(val) 52 | } 53 | 54 | @action updateFromText() { 55 | if (this.textValue == this.value) { return; } 56 | this.onChange(this.textValue) 57 | } 58 | 59 | @action updateValue(evt) { 60 | evt.stopPropagation(); 61 | this.set('focused', false); 62 | let val = evt.target.value; 63 | if (this.value == val) { return; } 64 | this.onChange(evt.target.value); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /ui/lib/apimas-docs/addon/components/doc-item-value/template.hbs: -------------------------------------------------------------------------------- 1 | {{# if (eq meta.action "choices") }} 2 | 8 | {{/if}} 9 | {{# if (eq meta.action "compute_element") }} 10 | 11 | {{/if }} 12 | {{#if (eq meta.mode "auto")}} 13 | {{ value }} 14 | {{/if}} 15 | {{#if (and (eq meta.type "string") (not meta.action))}} 16 | {{ input value=value focusOut=( action 'updateValue' ) }} 17 | {{/if}} 18 | {{#if (and (eq meta.type "int") (not meta.action))}} 19 | {{ input value=value type="number" focusOut=(action 'updateValue') }} 20 | {{/if}} 21 | {{#if (not meta) }} 22 | {{ value }} 23 | {{/if}} 24 | -------------------------------------------------------------------------------- /ui/lib/apimas-docs/addon/components/doc-view-item/component.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import template from './template'; 3 | import { layout, tagName, className, classNames } from '@ember-decorators/component'; 4 | import { computed } from '@ember-decorators/object'; 5 | 6 | @layout(template) 7 | @tagName('li') 8 | @classNames('doc-item') 9 | export default class DocViewItemComponent extends Component { 10 | @className 11 | @computed('expanded', 'depth') 12 | get depthClass() { 13 | let depth = this.depth > 2 ? `nested nested-${this.depth-2}` : '' 14 | let expanded = this.isDoc && this.expanded ? 'expanded' : ''; 15 | return depth + ' ' + expanded; 16 | } 17 | 18 | @className 19 | @computed('analysis.label') 20 | get analysisLabel() { 21 | let label = this.analysis && this.analysis.label; 22 | if (this.completed) { return 'status-completed'; } 23 | if (label) { return `status-${label}`; } 24 | return '' 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ui/lib/apimas-docs/addon/components/doc-view-item/template.hbs: -------------------------------------------------------------------------------- 1 |
{{ path }}{{ yield to="inverse"}}{{#if (or (eq meta.action "compute_element") (not isDoc)) }}{{#if (not isDoc)}}={{/if}}{{doc-item-value value=doc meta=meta analysis=analysis onChange=onChange tagName="span"}}{{/if}}{{type}}
2 | {{ yield }} 3 | -------------------------------------------------------------------------------- /ui/lib/apimas-docs/addon/components/doc-view/component.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import template from './template'; 3 | import { layout, tagName, classNames } from '@ember-decorators/component'; 4 | import { action, computed } from '@ember-decorators/object'; 5 | import { reads, gt, intersect, equal } from '@ember-decorators/object/computed'; 6 | import { typeOf } from '@ember/utils'; 7 | import { argument } from '@ember-decorators/argument'; 8 | 9 | 10 | @tagName('ul') 11 | @layout(template) 12 | @classNames('doc-view') 13 | export default class DocViewComponent extends Component { 14 | @argument doc; 15 | @argument root = ''; 16 | @argument key = null; 17 | @argument itemTagName = 'div'; 18 | @argument docTagName = 'div'; 19 | @argument itemComponent = 'doc-view-item'; 20 | @argument docComponent = 'doc-view'; 21 | @argument expandedPaths = []; 22 | @argument meta = {}; 23 | @argument completed = false; 24 | 25 | @argument('action') onExpand = function() {}; 26 | @argument('action') onCollapse = function() {}; 27 | @argument('action') onItemClick = function() {}; 28 | @argument('action') onValueChange = function() {}; 29 | @argument('action') onKeyLock = function() {}; 30 | 31 | @computed('doc') 32 | get docType() { return typeOf(this.doc); } 33 | 34 | @computed('path') 35 | get pathParts() { return this.path.split('/'); } 36 | @reads('pathParts.length') depth; 37 | 38 | @computed('root', 'key') 39 | get path() { if (!this.key) { return '' } return this.root + '/' + this.key; } 40 | 41 | @computed('keys.[]') 42 | get items() { 43 | return this.keys.map((key) => { 44 | let value = this.doc[key]; 45 | let type = typeOf(value); 46 | let isDoc = (type == 'object' || type == 'array'); 47 | let path = [this.path, key].join('/') 48 | let root = this.path 49 | return {key, value, isDoc, path, root, type}; 50 | }) 51 | } 52 | 53 | @computed('items.[]', 'expandedPaths.[]') 54 | get expandedItems() { 55 | let paths = this.expandedPaths; 56 | return this.items.filter(({ path }) => { return paths.includes(path); }) 57 | } 58 | 59 | @computed('keys.[]', 'path') 60 | get paths() { 61 | return this.keys.map((key) => { return [this.path, key].join('/'); }) 62 | } 63 | 64 | @intersect('paths', 'expandedPaths') selfExpandedPaths; 65 | @equal('selfExpandedPaths.length', 0) canExpandAll; 66 | 67 | @computed('doc', 'docType') 68 | get keys() { 69 | if (this.docType == 'object') { 70 | return Object.keys(this.doc); 71 | } else if (this.docType == 'array') { 72 | return Object.keys(this.doc); 73 | } else { 74 | return []; 75 | } 76 | } 77 | set keys(val) { return val } 78 | 79 | didReceiveAttrs() { 80 | this._super(...arguments); 81 | if (this.isDoc) { 82 | let newKeys = Object.keys(this.doc); 83 | if (newKeys.len != this.keys) { 84 | this.keys.setObjects(newKeys); 85 | } 86 | } 87 | } 88 | 89 | @gt('keys.length', 0) isDoc; 90 | 91 | 92 | @action expandPath(path=null) { 93 | if (!this.onExpand) { return } 94 | if (!path) { path = this.path } 95 | this.onExpand(path); 96 | } 97 | 98 | @action collapsePath(path=null) { 99 | if (!this.onCollapse) { return } 100 | if (!path) { path = this.path } 101 | this.onCollapse(path); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /ui/lib/apimas-docs/addon/components/doc-view/template.hbs: -------------------------------------------------------------------------------- 1 | {{#each-in doc as |itemKey itemValue|}} 2 | {{#with (doc-params doc path itemKey itemValue (get meta (concat path '/' itemKey))) as |item|}} 3 | {{#with (hash 4 | item=(component 5 | itemComponent 6 | root=path 7 | tagName=itemTagName 8 | doc=item.value 9 | key=item.key 10 | meta=(get meta item.path) 11 | path=item.path 12 | root=item.root 13 | type=item.type 14 | isDoc=item.isDoc 15 | completed=(if completed completed (get meta (concat item.path '.completed'))) 16 | disabled=(if disabled disabled (get meta (concat item.path '.disabled'))) 17 | depth=depth 18 | expanded=(contains item.path expandedPaths) 19 | expandPath=(action 'expandPath' item.path) 20 | collapsePath=(action 'collapsePath' item.path) 21 | onClick=(if item.isDoc (action onItemClick item)) 22 | onChange=(action onValueChange item) 23 | onLock=(action onKeyLock item) 24 | ) 25 | doc=(if 26 | item.isDoc 27 | (component 28 | docComponent 29 | root=path 30 | doc=item.value 31 | meta=meta 32 | completed=(if completed completed (get meta (concat item.path '.completed'))) 33 | disabled=(if disabled disabled (get meta (concat item.path '.disabled'))) 34 | tagName=docTagName 35 | itemTagName=itemTagName 36 | docTagName=docTagName 37 | itemComponent=itemComponent 38 | expandedPaths=expandedPaths 39 | expandPath=(action 'expandPath' item.path) 40 | collapsePath=(action 'collapsePath' item.path) 41 | onItemClick=onItemClick 42 | onValueChange=onValueChange 43 | onKeylock=onKeyLock 44 | key=item.key)) 45 | expanded=(contains item.path expandedPaths) 46 | completed=(if completed completed (get meta (concat item.path '.completed'))) 47 | disabled=(if disabled disabled (get meta (concat item.path '.disabled'))) 48 | meta=(get meta item.path) 49 | data=item) 50 | as |itemParams| 51 | }} 52 | 53 | {{#if hasBlock}} 54 | {{ yield itemParams }} 55 | {{else}} 56 | {{#itemParams.item }} 57 | {{#if item.isDoc}} 58 | {{#if itemParams.expanded}} 59 | {{itemParams.doc}} 60 | {{/if}} 61 | {{/if}} 62 | {{~else}} 63 | {{#if item.isDoc}} 64 | {{~#if itemParams.expanded}}↓{{~else}}→{{~/if}} 65 | {{/if}} 66 | {{/itemParams.item}} 67 | {{/if}} 68 | {{/with}} 69 | {{/with}} 70 | {{/each-in}} 71 | -------------------------------------------------------------------------------- /ui/lib/apimas-docs/addon/helpers/doc-params.js: -------------------------------------------------------------------------------- 1 | import { helper } from '@ember/component/helper'; 2 | import { typeOf } from '@ember/utils'; 3 | 4 | export function docParams([doc, root, key, value, meta]) { 5 | let type = typeOf(value); 6 | if (meta && meta.hasOwnProperty('type')) { type = meta.type; } 7 | let isDoc = ['array', 'object', 'dict'].includes(type); 8 | let path = [root, key].join('/'); 9 | return { 10 | value, key, isDoc, path, root, type 11 | } 12 | } 13 | 14 | export default helper(docParams); 15 | -------------------------------------------------------------------------------- /ui/lib/apimas-docs/addon/utils.js: -------------------------------------------------------------------------------- 1 | import { set } from '@ember/object'; 2 | import { typeOf } from '@ember/utils'; 3 | 4 | export function updateObject(target, source, removeMissing) { 5 | let targetKeys = Object.keys(target); 6 | let sourceKeys = Object.keys(source); 7 | sourceKeys.forEach((key) => { 8 | let targetVal = target[key]; 9 | let sourceVal = source[key]; 10 | let targetType = typeOf(targetVal); 11 | let sourceType = typeOf(sourceVal); 12 | 13 | if (targetType != sourceType) { 14 | set(target, key, sourceVal); 15 | } else if (targetType == "object") { 16 | updateObject(targetVal, sourceVal, removeMissing); 17 | } else if (targetType == "array") { 18 | updateObject(targetVal, sourceVal, removeMissing); 19 | } else if (targetVal != sourceVal) { 20 | set(target, key, sourceVal); 21 | } 22 | }); 23 | 24 | if (removeMissing) { 25 | targetKeys.forEach((key) => { 26 | if (!sourceKeys.includes(key)) { 27 | delete target[key]; 28 | } 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ui/lib/apimas-docs/app/components/doc-item-value/component.js: -------------------------------------------------------------------------------- 1 | export { default } from 'apimas-docs/components/doc-item-value/component'; -------------------------------------------------------------------------------- /ui/lib/apimas-docs/app/components/doc-view-item/component.js: -------------------------------------------------------------------------------- 1 | export { default } from 'apimas-docs/components/doc-view-item/component'; -------------------------------------------------------------------------------- /ui/lib/apimas-docs/app/components/doc-view/component.js: -------------------------------------------------------------------------------- 1 | export { default } from 'apimas-docs/components/doc-view/component'; -------------------------------------------------------------------------------- /ui/lib/apimas-docs/app/helpers/doc-params.js: -------------------------------------------------------------------------------- 1 | export { default, docParams } from 'apimas-docs/helpers/doc-params'; 2 | -------------------------------------------------------------------------------- /ui/lib/apimas-docs/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 'use strict'; 3 | 4 | module.exports = { 5 | name: require('./package').name, 6 | 7 | isDevelopingAddon() { 8 | return true; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /ui/lib/apimas-docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apimas-docs", 3 | "keywords": [ 4 | "ember-addon" 5 | ], 6 | "dependencies": { 7 | "@ember-decorators/babel-transforms": "^2.1.2", 8 | "ember-cli-htmlbars": "^3.0.0", 9 | "ember-cli-babel": "^6.17.2", 10 | "ember-paper": "^1.0.0-beta.18", 11 | "ember-paper-expansion-panel": "0.0.4" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zeus", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "Small description for zeustmp goes here", 6 | "repository": "", 7 | "license": "MIT", 8 | "author": "", 9 | "directories": { 10 | "doc": "doc", 11 | "test": "tests" 12 | }, 13 | "scripts": { 14 | "build": "ember build", 15 | "lint:hbs": "ember-template-lint .", 16 | "lint:js": "eslint .", 17 | "start": "ember serve", 18 | "test": "ember test" 19 | }, 20 | "devDependencies": { 21 | "@ember-decorators/argument": "^0.8.21", 22 | "@ember-decorators/babel-transforms": "^2.1.2", 23 | "@ember/jquery": "^0.5.2", 24 | "@ember/optional-features": "^0.6.3", 25 | "babel-eslint": "^8.2.6", 26 | "broccoli-asset-rev": "^2.7.0", 27 | "ember-ajax": "^3.1.0", 28 | "ember-cli": "~3.5.0", 29 | "ember-cli-app-version": "^3.2.0", 30 | "ember-cli-babel": "^6.17.2", 31 | "ember-cli-dependency-checker": "^3.0.0", 32 | "ember-cli-eslint": "^4.2.3", 33 | "ember-cli-htmlbars": "^3.0.0", 34 | "ember-cli-htmlbars-inline-precompile": "^1.0.3", 35 | "ember-cli-inject-live-reload": "^1.8.2", 36 | "ember-cli-sass": "^8.0.1", 37 | "ember-cli-sri": "^2.1.1", 38 | "ember-cli-template-lint": "^1.0.0-beta.1", 39 | "ember-cli-uglify": "^2.1.0", 40 | "ember-composable-helpers": "^2.1.0", 41 | "ember-data": "~3.4.4", 42 | "ember-decorators": "^2.5.2", 43 | "ember-export-application-global": "^2.0.0", 44 | "ember-flatpickr": "^2.12.0", 45 | "ember-load-initializers": "^1.1.0", 46 | "ember-maybe-import-regenerator": "^0.1.6", 47 | "ember-paper": "^1.0.0-beta.18", 48 | "ember-paper-expansion-panel": "0.0.4", 49 | "ember-qunit": "^3.4.1", 50 | "ember-resolver": "^5.0.1", 51 | "ember-source": "~3.5.0", 52 | "ember-truth-helpers": "^2.1.0", 53 | "eslint-plugin-ember": "^5.2.0", 54 | "loader.js": "^4.7.0", 55 | "qunit-dom": "^0.8.0", 56 | "sass": "^1.15.1" 57 | }, 58 | "engines": { 59 | "node": "6.* || 8.* || >= 10.*" 60 | }, 61 | "ember-addon": { 62 | "paths": [ 63 | "lib/apimas-docs" 64 | ] 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /ui/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /ui/testem.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | test_page: 'tests/index.html?hidepassed', 3 | disable_watching: true, 4 | launch_in_ci: [ 5 | 'Chrome' 6 | ], 7 | launch_in_dev: [ 8 | 'Chrome' 9 | ], 10 | browser_args: { 11 | Chrome: { 12 | ci: [ 13 | // --no-sandbox is needed when running Chrome inside a container 14 | process.env.CI ? '--no-sandbox' : null, 15 | '--headless', 16 | '--disable-gpu', 17 | '--disable-dev-shm-usage', 18 | '--disable-software-rasterizer', 19 | '--mute-audio', 20 | '--remote-debugging-port=0', 21 | '--window-size=1440,900' 22 | ].filter(Boolean) 23 | } 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /ui/tests/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grnet/panoramix/a9b788e1a073e106d5542511094fe4ac3828d4b6/ui/tests/helpers/.gitkeep -------------------------------------------------------------------------------- /ui/tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Zeus Tests 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | {{content-for "test-head"}} 12 | 13 | 14 | 15 | 16 | 17 | {{content-for "head-footer"}} 18 | {{content-for "test-head-footer"}} 19 | 20 | 21 | {{content-for "body"}} 22 | {{content-for "test-body"}} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {{content-for "body-footer"}} 31 | {{content-for "test-body-footer"}} 32 | 33 | 34 | -------------------------------------------------------------------------------- /ui/tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import Application from '../app'; 2 | import config from '../config/environment'; 3 | import { setApplication } from '@ember/test-helpers'; 4 | import { start } from 'ember-qunit'; 5 | 6 | setApplication(Application.create(config.APP)); 7 | 8 | start(); 9 | -------------------------------------------------------------------------------- /ui/vendor/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grnet/panoramix/a9b788e1a073e106d5542511094fe4ac3828d4b6/ui/vendor/.gitkeep -------------------------------------------------------------------------------- /version.txt: -------------------------------------------------------------------------------- 1 | 0.3 2 | -------------------------------------------------------------------------------- /zeus_trust/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grnet/panoramix/a9b788e1a073e106d5542511094fe4ac3828d4b6/zeus_trust/__init__.py -------------------------------------------------------------------------------- /zeus_trust/spec.py: -------------------------------------------------------------------------------- 1 | import specular 2 | 3 | 4 | negotiation_specs = [ 5 | ('.negotiation_stage', 6 | { 7 | '.negotiation_stage': {}, 8 | 'type': {}, 9 | 'negotiation': {}, 10 | 'signers': {}, 11 | 'content': {}, 12 | }, 13 | ), 14 | 15 | ('.negotiation', 16 | { 17 | '.negotiation': {}, 18 | 'stages': { 19 | '?': {'.negotiation_stage': {}} 20 | }, 21 | }, 22 | ), 23 | 24 | ('.peer', 25 | {'.peer': {}}, 26 | ), 27 | 28 | ('.cryptosystem', 29 | {'.cryptosystem': {}}, 30 | ), 31 | 32 | 33 | ('.sign_key', 34 | {'.sign_key': {}}, 35 | ), 36 | 37 | ('.election_key', 38 | {'.election_key': {}}, 39 | ), 40 | 41 | ('.shares', 42 | {'.shares': {}}, 43 | ), 44 | 45 | ('.mixes', 46 | {'.mixes': {}}, 47 | ), 48 | 49 | ('.decryptions', 50 | {'.decryptions': {}}, 51 | ), 52 | ] 53 | 54 | 55 | negotiation_zeus_spec = { 56 | '.negotiation.zeus': {}, 57 | 'stages': { 58 | 'stage_A': { 59 | 'type': 'authority', 60 | 'title': 'Trustees', 61 | 'description': "Record trustees' identity", 62 | 'negotiation': {}, 63 | 'peers': 'trustees', 64 | 'signers': ['stage_A/trustees'], 65 | 'content': { 66 | 'last_consensus': {}, 67 | 'signing_cryptosystem': {'.cryptosystem': {}}, 68 | 'trustees': { 69 | '?': {'.sign_key': {}}, 70 | }, 71 | }, 72 | }, 73 | 'stage_B': { 74 | 'type': 'configuration', 75 | 'title': 'Election', 76 | 'description': 'Configure election parameters', 77 | 'negotiation': {}, 78 | 'signers': ['stage_A/trustees'], 79 | 'content': { 80 | 'last_consensus': {}, 81 | 'election_cryptosystem': {'.cryptosystem': {}}, 82 | 'public_shares': { 83 | '.shares': {}, 84 | '?': {'.election_key': {}}, 85 | }, 86 | 'election_name': {}, 87 | # 'election_admin': {}, 88 | 'no_of_mixers': {}, 89 | 'mixnet': {}, 90 | }, 91 | }, 92 | 'stage_C': { 93 | 'type': 'mixnet_setup', 94 | 'title': 'Mixers', 95 | 'description': 'Record mixing peers', 96 | 'negotiation': {}, 97 | 'signers': ['stage_A/trustees', 'stage_C/mixers'], 98 | 'content': { 99 | 'last_consensus': {}, 100 | 'mixers': { 101 | '?': {'.peer': {}}, 102 | }, 103 | }, 104 | }, 105 | 'stage_D': { 106 | 'type': 'activation', 107 | 'title': 'Booth', 108 | 'description': 'Configure booth operation, record candidates', 109 | 'negotiation': {}, 110 | 'signers': ['stage_A/trustees'], 111 | 'content': { 112 | 'last_consensus': {}, 113 | 'opens_at': {}, 114 | 'closes_at': {}, 115 | 'candidates': {}, 116 | 'voters': {}, 117 | # 'excluded_voters': {}, 118 | }, 119 | }, 120 | 'stage_E': { 121 | 'type': 'voting', 122 | 'title': 'Voting', 123 | 'description': 'Give extension or close booth', 124 | 'negotiation': {}, 125 | 'signers': ['stage_A/trustees'], 126 | 'content': { 127 | 'last_consensus': {}, 128 | 'extensions': {}, 129 | }, 130 | }, 131 | 'stage_F': { 132 | 'type': 'votes', 133 | 'title': 'Votes', 134 | 'description': 'Record cast votes', 135 | 'negotiation': {}, 136 | 'signers': ['stage_A/trustees'], 137 | 'content': { 138 | 'last_consensus': {}, 139 | 'votes': {}, 140 | }, 141 | }, 142 | 'stage_G': { 143 | 'type': 'mixing', 144 | 'title': 'Mixes', 145 | 'description': 'Record results produced from the mixnet', 146 | 'negotiation': {}, 147 | 'signers': ['stage_A/trustees', 'stage_C/mixers'], 148 | 'content': { 149 | 'last_consensus': {}, 150 | 'mixes': { 151 | '.mixes': {}, 152 | '?': {}}, 153 | }, 154 | 'actions': ['decrypt'], 155 | }, 156 | 'stage_H': { 157 | 'type': 'decryption', 158 | 'title': 'Decryption', 159 | 'description': 'Record decryptions produced by the trustees', 160 | 'negotiation': {}, 161 | 'signers': ['stage_A/trustees'], 162 | 'content': { 163 | 'last_consensus': {}, 164 | 'decryptions': { 165 | '.decryptions': {}, 166 | '?': {}}, 167 | }, 168 | }, 169 | }, 170 | } 171 | 172 | domain = specular.Spec() 173 | domain.compile_schemata(negotiation_specs) 174 | domain.compile_schema('.negotiation.zeus', negotiation_zeus_spec) 175 | --------------------------------------------------------------------------------