Merge branch 'py3' into plugins

This commit is contained in:
Vadim Ushakov 2020-03-09 19:28:09 +07:00
commit 920ddd944f
222 changed files with 13121 additions and 7213 deletions

1
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1 @@
custom: https://zeronet.io/docs/help_zeronet/donate/

49
.github/workflows/tests.yml vendored Normal file
View file

@ -0,0 +1,49 @@
name: tests
on: [push]
jobs:
test:
runs-on: ubuntu-16.04
strategy:
max-parallel: 16
matrix:
python-version: [3.5, 3.6, 3.7, 3.8]
steps:
- uses: actions/checkout@v1
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Prepare for installation
run: |
python3 -m pip install setuptools
python3 -m pip install --upgrade pip wheel
python3 -m pip install --upgrade codecov coveralls flake8 mock pytest==4.6.3 pytest-cov selenium
- name: Install
run: |
python3 -m pip install --upgrade -r requirements.txt
python3 -m pip list
- name: Prepare for tests
run: |
openssl version -a
echo 0 | sudo tee /proc/sys/net/ipv6/conf/all/disable_ipv6
- name: Test
run: |
catchsegv python3 -m pytest src/Test --cov=src --cov-config src/Test/coverage.ini
export ZERONET_LOG_DIR="log/CryptMessage"; catchsegv python3 -m pytest -x plugins/CryptMessage/Test
export ZERONET_LOG_DIR="log/Bigfile"; catchsegv python3 -m pytest -x plugins/Bigfile/Test
export ZERONET_LOG_DIR="log/AnnounceLocal"; catchsegv python3 -m pytest -x plugins/AnnounceLocal/Test
export ZERONET_LOG_DIR="log/OptionalManager"; catchsegv python3 -m pytest -x plugins/OptionalManager/Test
export ZERONET_LOG_DIR="log/Multiuser"; mv plugins/disabled-Multiuser plugins/Multiuser && catchsegv python -m pytest -x plugins/Multiuser/Test
export ZERONET_LOG_DIR="log/Bootstrapper"; mv plugins/disabled-Bootstrapper plugins/Bootstrapper && catchsegv python -m pytest -x plugins/Bootstrapper/Test
find src -name "*.json" | xargs -n 1 python3 -c "import json, sys; print(sys.argv[1], end=' '); json.load(open(sys.argv[1])); print('[OK]')"
find plugins -name "*.json" | xargs -n 1 python3 -c "import json, sys; print(sys.argv[1], end=' '); json.load(open(sys.argv[1])); print('[OK]')"
flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pyaes/

5
.gitignore vendored
View file

@ -7,8 +7,10 @@ __pycache__/
# Hidden files
.*
!/.github
!/.gitignore
!/.travis.yml
!/.gitlab-ci.yml
# Temporary files
*.bak
@ -28,3 +30,6 @@ tools/phantomjs
# ZeroNet config file
zeronet.conf
# ZeroNet log files
log/*

48
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,48 @@
stages:
- test
.test_template: &test_template
stage: test
before_script:
- pip install --upgrade pip wheel
# Selenium and requests can't be installed without a requests hint on Python 3.4
- pip install --upgrade requests>=2.22.0
- pip install --upgrade codecov coveralls flake8 mock pytest==4.6.3 pytest-cov selenium
- pip install --upgrade -r requirements.txt
script:
- pip list
- openssl version -a
- python -m pytest -x plugins/CryptMessage/Test --color=yes
- python -m pytest -x plugins/Bigfile/Test --color=yes
- python -m pytest -x plugins/AnnounceLocal/Test --color=yes
- python -m pytest -x plugins/OptionalManager/Test --color=yes
- python -m pytest src/Test --cov=src --cov-config src/Test/coverage.ini --color=yes
- mv plugins/disabled-Multiuser plugins/Multiuser
- python -m pytest -x plugins/Multiuser/Test --color=yes
- mv plugins/disabled-Bootstrapper plugins/Bootstrapper
- python -m pytest -x plugins/Bootstrapper/Test --color=yes
- flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pyaes/
test:py3.4:
image: python:3.4.3
<<: *test_template
test:py3.5:
image: python:3.5.7
<<: *test_template
test:py3.6:
image: python:3.6.9
<<: *test_template
test:py3.7-openssl1.1.0:
image: python:3.7.0b5
<<: *test_template
test:py3.7-openssl1.1.1:
image: python:3.7.4
<<: *test_template
test:py3.8:
image: python:3.8.0b3
<<: *test_template

View file

@ -4,8 +4,7 @@ python:
- 3.5
- 3.6
- 3.7
- 3.8-dev
dist: xenial
- 3.8
services:
- docker
cache: pip
@ -25,14 +24,19 @@ before_script:
sudo sh -c 'echo 0 > /proc/sys/net/ipv6/conf/all/disable_ipv6';
fi
script:
- python -m pytest -x plugins/CryptMessage/Test
- python -m pytest -x plugins/Bigfile/Test
- python -m pytest -x plugins/AnnounceLocal/Test
- python -m pytest -x plugins/OptionalManager/Test
- python -m pytest src/Test --cov=src --cov-config src/Test/coverage.ini
- mv plugins/disabled-Multiuser plugins/Multiuser && python -m pytest -x plugins/Multiuser/Test
- mv plugins/disabled-Bootstrapper plugins/Bootstrapper && python -m pytest -x plugins/Bootstrapper/Test
- flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pybitcointools/
- catchsegv python -m pytest src/Test --cov=src --cov-config src/Test/coverage.ini
- export ZERONET_LOG_DIR="log/CryptMessage"; catchsegv python -m pytest -x plugins/CryptMessage/Test
- export ZERONET_LOG_DIR="log/Bigfile"; catchsegv python -m pytest -x plugins/Bigfile/Test
- export ZERONET_LOG_DIR="log/AnnounceLocal"; catchsegv python -m pytest -x plugins/AnnounceLocal/Test
- export ZERONET_LOG_DIR="log/OptionalManager"; catchsegv python -m pytest -x plugins/OptionalManager/Test
- export ZERONET_LOG_DIR="log/Multiuser"; mv plugins/disabled-Multiuser plugins/Multiuser && catchsegv python -m pytest -x plugins/Multiuser/Test
- export ZERONET_LOG_DIR="log/Bootstrapper"; mv plugins/disabled-Bootstrapper plugins/Bootstrapper && catchsegv python -m pytest -x plugins/Bootstrapper/Test
- find src -name "*.json" | xargs -n 1 python3 -c "import json, sys; print(sys.argv[1], end=' '); json.load(open(sys.argv[1])); print('[OK]')"
- find plugins -name "*.json" | xargs -n 1 python3 -c "import json, sys; print(sys.argv[1], end=' '); json.load(open(sys.argv[1])); print('[OK]')"
- flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pyaes/
after_failure:
- zip -r log.zip log/
- curl --upload-file ./log.zip https://transfer.sh/log.zip
after_success:
- codecov
- coveralls --rcfile=src/Test/coverage.ini

View file

@ -1,48 +1,46 @@
# ZeroNet [![Build Status](https://travis-ci.org/HelloZeroNet/ZeroNet.svg?branch=master)](https://travis-ci.org/HelloZeroNet/ZeroNet) [![Documentation](https://img.shields.io/badge/docs-faq-brightgreen.svg)](https://zeronet.io/docs/faq/) [![Help](https://img.shields.io/badge/keep_this_project_alive-donate-yellow.svg)](https://zeronet.io/docs/help_zeronet/donate/)
# ZeroNet [![Build Status](https://travis-ci.org/HelloZeroNet/ZeroNet.svg?branch=py3)](https://travis-ci.org/HelloZeroNet/ZeroNet) [![Documentation](https://img.shields.io/badge/docs-faq-brightgreen.svg)](https://zeronet.io/docs/faq/) [![Help](https://img.shields.io/badge/keep_this_project_alive-donate-yellow.svg)](https://zeronet.io/docs/help_zeronet/donate/)
[English](./README.md)
使用 Bitcoin 加密和 BitTorrent 网络的去中心化网络 - https://zeronet.io
## 为什么?
## 为什么
* 我们相信开放,自由,无审查的网络
* 我们相信开放,自由,无审查的网络和通讯
* 不会受单点故障影响:只要有在线的节点,站点就会保持在线
* 无托管费用: 站点由访问者托管
* 无法关闭: 因为节点无处不在
* 快速并可离线运行: 即使没有互联网连接也可以使用
* 无托管费用站点由访问者托管
* 无法关闭因为节点无处不在
* 快速并可离线运行即使没有互联网连接也可以使用
## 功能
* 实时站点更新
* 支持 Namecoin 的 .bit 域名
* 安装方便: 只需解压并运行
* 安装方便只需解压并运行
* 一键克隆存在的站点
* 无需密码、基于 [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) 的认证:用与比特币钱包相同的加密方法用来保护你的账户
你的账户被使用和比特币钱包相同的加密方法
* 内建 SQL 服务器和 P2P 数据同步: 让开发更简单并提升加载速度
* 匿名性: 完整的 Tor 网络支持,支持通过 .onion 隐藏服务相互连接而不是通过IPv4地址连接
* 无需密码、基于 [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki)
的认证:您的账户被与比特币钱包相同的加密方法保护
* 内建 SQL 服务器和 P2P 数据同步让开发更简单并提升加载速度
* 匿名性完整的 Tor 网络支持,支持通过 .onion 隐藏服务相互连接而不是通过 IPv4 地址连接
* TLS 加密连接
* 自动打开 uPnP 端口
* 插件和多用户 (开放式代理) 支持
* 全平台兼容
* 多用户openproxy支持的插件
* 适用于任何浏览器 / 操作系统
## 原理
* 在你运行`zeronet.py`后你将可以通过`http://127.0.0.1:43110/{zeronet_address}` (比如.
`http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D`)。访问 zeronet 中的站点。
* 在运行 `zeronet.py` 后,您将可以通过
`http://127.0.0.1:43110/{zeronet_address}`(例如:
`http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D`)访问 zeronet 中的站点
* 在您浏览 zeronet 站点时,客户端会尝试通过 BitTorrent 网络来寻找可用的节点从而下载需要的文件htmlcssjs...
* 您将会储存每一个浏览过的站点
* 每个站点都包含一个名为 `content.json` 的文件,它储存了其他所有文件的 sha512 散列值以及一个通过站点私钥生成的签名
* 如果站点的所有者(拥有站点地址的私钥)修改了站点,并且他 / 她签名了新的 `content.json` 然后推送至其他节点,
那么这些节点将会在使用签名验证 `content.json` 的真实性后,下载修改后的文件并将新内容推送至另外的节点
* 在你浏览 zeronet 站点时,客户端会尝试通过 BitTorrent 网络来寻找可用的节点,从而下载需要的文件 (html, css, js...)
* 你将会储存每一个浏览过的站点
* 每个站点都包含一个名为 `content.json` ,它储存了其他所有文件的 sha512 hash 值
和一个通过站点私钥建立的签名
* 如果站点的所有者 (拥有私钥的那个人) 修改了站点, 并且他/她签名了新的 `content.json` 然后推送至其他节点,
那么所有节点将会在验证 `content.json` 的真实性 (使用签名)后, 下载修改后的文件并推送至其他节点。
#### [有关于 ZeroNet 加密, 站点更新, 多用户站点的幻灯片 »](https://docs.google.com/presentation/d/1qBxkroB_iiX2zHEn0dt-N-qRZgyEzui46XS2hEa3AA4/pub?start=false&loop=false&delayms=3000)
#### [关于 ZeroNet 加密,站点更新,多用户站点的幻灯片 »](https://docs.google.com/presentation/d/1_2qK1IuOKJ51pgBvllZ9Yu7Au2l551t3XBgyTSvilew/pub?start=false&loop=false&delayms=3000)
#### [常见问题 »](https://zeronet.io/docs/faq/)
#### [ZeroNet 开发者文档 »](https://zeronet.io/docs/site_development/getting_started/)
@ -53,123 +51,60 @@
![Screenshot](https://i.imgur.com/H60OAHY.png)
![ZeroTalk](https://zeronet.io/docs/img/zerotalk.png)
#### [在 ZeroNet 文档里查看更多的屏幕截图 »](https://zeronet.io/docs/using_zeronet/sample_sites/)
#### [ZeroNet 文档中的更多屏幕截图 »](https://zeronet.io/docs/using_zeronet/sample_sites/)
## 如何加入
## 如何加入
* 下载 ZeroBundle 文件包:
* [Microsoft Windows](https://github.com/HelloZeroNet/ZeroNet-win/archive/dist/ZeroNet-win.zip)
* [Apple macOS](https://github.com/HelloZeroNet/ZeroNet-mac/archive/dist/ZeroNet-mac.zip)
* [Linux 64bit](https://github.com/HelloZeroNet/ZeroBundle/raw/master/dist/ZeroBundle-linux64.tar.gz)
* [Linux 32bit](https://github.com/HelloZeroNet/ZeroBundle/raw/master/dist/ZeroBundle-linux32.tar.gz)
* 解压缩
* 运行 `ZeroNet.exe` (win), `ZeroNet(.app)` (osx), `ZeroNet.sh` (linux)
### Windows
### Linux 命令行
- 下载 [ZeroNet-py3-win64.zip](https://github.com/HelloZeroNet/ZeroNet-win/archive/dist-win64/ZeroNet-py3-win64.zip) (18MB)
- 在任意位置解压缩
- 运行 `ZeroNet.exe`
* `wget https://github.com/HelloZeroNet/ZeroBundle/raw/master/dist/ZeroBundle-linux64.tar.gz`
* `tar xvpfz ZeroBundle-linux64.tar.gz`
* `cd ZeroBundle`
* 执行 `./ZeroNet.sh` 来启动
### macOS
在你打开时他将会自动下载最新版本的 ZeroNet 。
- 下载 [ZeroNet-dist-mac.zip](https://github.com/HelloZeroNet/ZeroNet-dist/archive/mac/ZeroNet-dist-mac.zip) (13.2MB)
- 在任意位置解压缩
- 运行 `ZeroNet.app`
#### 在 Debian Linux 中手动安装
### Linux (x86-64bit)
* `sudo apt-get update`
* `sudo apt-get install msgpack-python python-gevent`
* `wget https://github.com/HelloZeroNet/ZeroNet/archive/master.tar.gz`
* `tar xvpfz master.tar.gz`
* `cd ZeroNet-master`
* 执行 `python2 zeronet.py` 来启动
* 在你的浏览器中打开 http://127.0.0.1:43110/
- `wget https://github.com/HelloZeroNet/ZeroNet-linux/archive/dist-linux64/ZeroNet-py3-linux64.tar.gz`
- `tar xvpfz ZeroNet-py3-linux64.tar.gz`
- `cd ZeroNet-linux-dist-linux64/`
- 使用以下命令启动 `./ZeroNet.sh`
- 在浏览器打开 http://127.0.0.1:43110/ 即可访问 ZeroHello 页面
### [FreeBSD](https://www.freebsd.org/)
__提示__ 若要允许在 Web 界面上的远程连接,使用以下命令启动 `./ZeroNet.sh --ui_ip '*' --ui_restrict your.ip.address`
* `pkg install zeronet` 或者 `cd /usr/ports/security/zeronet/ && make install clean`
* `sysrc zeronet_enable="YES"`
* `service zeronet start`
* 在你的浏览器中打开 http://127.0.0.1:43110/
### 从源代码安装
### [Vagrant](https://www.vagrantup.com/)
* `vagrant up`
* 通过 `vagrant ssh` 连接到 VM
* `cd /vagrant`
* 运行 `python2 zeronet.py --ui_ip 0.0.0.0`
* 在你的浏览器中打开 http://127.0.0.1:43110/
### [Docker](https://www.docker.com/)
* `docker run -d -v <local_data_folder>:/root/data -p 26552:26552 -p 43110:43110 nofish/zeronet`
* 这个 Docker 镜像包含了 Tor ,但默认是禁用的,因为一些托管商不允许你在他们的服务器上运行 Tor。如果你希望启用它
设置 `ENABLE_TOR` 环境变量为 `true` (默认: `false`). E.g.:
`docker run -d -e "ENABLE_TOR=true" -v <local_data_folder>:/root/data -p 26552:26552 -p 43110:43110 nofish/zeronet`
* 在你的浏览器中打开 http://127.0.0.1:43110/
### [Virtualenv](https://virtualenv.readthedocs.org/en/latest/)
* `virtualenv env`
* `source env/bin/activate`
* `pip install msgpack gevent`
* `python2 zeronet.py`
* 在你的浏览器中打开 http://127.0.0.1:43110/
- `wget https://github.com/HelloZeroNet/ZeroNet/archive/py3/ZeroNet-py3.tar.gz`
- `tar xvpfz ZeroNet-py3.tar.gz`
- `cd ZeroNet-py3`
- `sudo apt-get update`
- `sudo apt-get install python3-pip`
- `sudo python3 -m pip install -r requirements.txt`
- 使用以下命令启动 `python3 zeronet.py`
- 在浏览器打开 http://127.0.0.1:43110/ 即可访问 ZeroHello 页面
## 现有限制
* ~~没有类似于 BitTorrent 的文件拆分来支持大文件~~ (已添加大文件支持)
* ~~没有比 BitTorrent 更好的匿名性~~ (已添加内置的完整 Tor 支持)
* 传输文件时没有压缩~~和加密~~ (已添加 TLS 支持)
* ~~没有类似于 torrent 的文件拆分来支持大文件~~ (已添加大文件支持)
* ~~没有比 BitTorrent 更好的匿名性~~ (已添加内置的完整 Tor 支持)
* 传输文件时没有压缩~~和加密~~ (已添加 TLS 支持)
* 不支持私有站点
## 如何创建一个 ZeroNet 站点?
## 如何创建一个 ZeroNet 站点?
* 点击 [ZeroHello](http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D) 站点的 **⋮** > **「新建空站点」** 菜单项
* 您将被**重定向**到一个全新的站点,该站点只能由您修改
* 您可以在 **data/[您的站点地址]** 目录中找到并修改网站的内容
* 修改后打开您的网站将右上角的「0」按钮拖到左侧然后点击底部的**签名**并**发布**按钮
如果 zeronet 在运行,把它关掉
执行:
```bash
$ zeronet.py siteCreate
...
- Site private key: 23DKQpzxhbVBrAtvLEc2uvk7DZweh4qL3fn3jpM3LgHDczMK2TtYUq
- Site address: 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2
...
- Site created!
$ zeronet.py
...
```
你已经完成了! 现在任何人都可以通过
`http://localhost:43110/13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2`
来访问你的站点
下一步: [ZeroNet 开发者文档](https://zeronet.io/docs/site_development/getting_started/)
## 我要如何修改 ZeroNet 站点?
* 修改位于 data/13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2 的目录.
在你改好之后:
```bash
$ zeronet.py siteSign 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2
- Signing site: 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2...
Private key (input hidden):
```
* 输入你在创建站点时获得的私钥
```bash
$ zeronet.py sitePublish 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2
...
Site:13DNDk..bhC2 Publishing to 3/10 peers...
Site:13DNDk..bhC2 Successfuly published to 3 peers
- Serving files....
```
* 就是这样! 你现在已经成功的签名并推送了你的更改。
接下来的步骤:[ZeroNet 开发者文档](https://zeronet.io/docs/site_development/getting_started/)
## 帮助这个项目
@ -178,11 +113,11 @@ Site:13DNDk..bhC2 Successfuly published to 3 peers
### 赞助商
* 在 OSX/Safari 下 [BrowserStack.com](https://www.browserstack.com) 带来更好的兼容性
* [BrowserStack.com](https://www.browserstack.com) 使更好的 macOS/Safari 兼容性成为可能
#### 感谢!
#### 感谢您!
* 更多信息, 帮助, 变更记录和 zeronet 站点: https://www.reddit.com/r/zeronet/
* 在: [#zeronet @ FreeNode](https://kiwiirc.com/client/irc.freenode.net/zeronet) 和我们聊天,或者使用 [gitter](https://gitter.im/HelloZeroNet/ZeroNet)
* 更多信息,帮助,变更记录和 zeronet 站点:https://www.reddit.com/r/zeronet/
* 前往 [#zeronet @ FreeNode](https://kiwiirc.com/client/irc.freenode.net/zeronet) 或 [gitter](https://gitter.im/HelloZeroNet/ZeroNet) 和我们聊天
* [这里](https://gitter.im/ZeroNet-zh/Lobby)是一个 gitter 上的中文聊天室
* Email: hello@noloop.me
* Email: hello@zeronet.io (PGP: [960F FF2D 6C14 5AA6 13E8 491B 5B63 BAE6 CB96 13AE](https://zeronet.io/files/tamas@zeronet.io_pub.asc))

View file

@ -1,4 +1,4 @@
# ZeroNet [![Build Status](https://travis-ci.org/HelloZeroNet/ZeroNet.svg?branch=master)](https://travis-ci.org/HelloZeroNet/ZeroNet) [![Documentation](https://img.shields.io/badge/docs-faq-brightgreen.svg)](https://zeronet.io/docs/faq/) [![Help](https://img.shields.io/badge/keep_this_project_alive-donate-yellow.svg)](https://zeronet.io/docs/help_zeronet/donate/)
# ZeroNet [![Build Status](https://travis-ci.org/HelloZeroNet/ZeroNet.svg?branch=py3)](https://travis-ci.org/HelloZeroNet/ZeroNet) [![Documentation](https://img.shields.io/badge/docs-faq-brightgreen.svg)](https://zeronet.io/docs/faq/) [![Help](https://img.shields.io/badge/keep_this_project_alive-donate-yellow.svg)](https://zeronet.io/docs/help_zeronet/donate/)
Decentralized websites using Bitcoin crypto and the BitTorrent network - https://zeronet.io
@ -67,39 +67,31 @@ Decentralized websites using Bitcoin crypto and the BitTorrent network - https:/
- Unpack anywhere
- Run `ZeroNet.exe`
### Other platforms: Install from source
### macOS
Fetch and extract the source:
- Download [ZeroNet-dist-mac.zip](https://github.com/HelloZeroNet/ZeroNet-dist/archive/mac/ZeroNet-dist-mac.zip) (13.2MB)
- Unpack anywhere
- Run `ZeroNet.app`
wget https://github.com/HelloZeroNet/ZeroNet/archive/py3/ZeroNet-py3.tar.gz
tar xvpfz ZeroNet-py3.tar.gz
cd ZeroNet-py3
### Linux (x86-64bit)
- `wget https://github.com/HelloZeroNet/ZeroNet-linux/archive/dist-linux64/ZeroNet-py3-linux64.tar.gz`
- `tar xvpfz ZeroNet-py3-linux64.tar.gz`
- `cd ZeroNet-linux-dist-linux64/`
- Start with: `./ZeroNet.sh`
- Open the ZeroHello landing page in your browser by navigating to: http://127.0.0.1:43110/
Install Python module dependencies either:
__Tip:__ Start with `./ZeroNet.sh --ui_ip '*' --ui_restrict your.ip.address` to allow remote connections on the web interface.
* (Option A) into a [virtual env](https://virtualenv.readthedocs.org/en/latest/)
### Install from source
```
virtualenv zeronet
source zeronet/bin/activate
python -m pip install -r requirements.txt
```
* (Option B) into the system (requires root), for example, on Debian/Ubuntu:
```
sudo apt-get update
sudo apt-get install python3-pip
sudo python3 -m pip install -r requirements.txt
```
Start Zeronet:
python3 zeronet.py
Open the ZeroHello landing page in your browser by navigating to:
http://127.0.0.1:43110/
- `wget https://github.com/HelloZeroNet/ZeroNet/archive/py3/ZeroNet-py3.tar.gz`
- `tar xvpfz ZeroNet-py3.tar.gz`
- `cd ZeroNet-py3`
- `sudo apt-get update`
- `sudo apt-get install python3-pip`
- `sudo python3 -m pip install -r requirements.txt`
- Start with: `python3 zeronet.py`
- Open the ZeroHello landing page in your browser by navigating to: http://127.0.0.1:43110/
## Current limitations
@ -111,49 +103,13 @@ Open the ZeroHello landing page in your browser by navigating to:
## How can I create a ZeroNet site?
Shut down zeronet if you are running it already
```bash
$ zeronet.py siteCreate
...
- Site private key: 23DKQpzxhbVBrAtvLEc2uvk7DZweh4qL3fn3jpM3LgHDczMK2TtYUq
- Site address: 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2
...
- Site created!
$ zeronet.py
...
```
Congratulations, you're finished! Now anyone can access your site using
`http://localhost:43110/13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2`
* Click on **⋮** > **"Create new, empty site"** menu item on the site [ZeroHello](http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D).
* You will be **redirected** to a completely new site that is only modifiable by you!
* You can find and modify your site's content in **data/[yoursiteaddress]** directory
* After the modifications open your site, drag the topright "0" button to left, then press **sign** and **publish** buttons on the bottom
Next steps: [ZeroNet Developer Documentation](https://zeronet.io/docs/site_development/getting_started/)
## How can I modify a ZeroNet site?
* Modify files located in data/13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2 directory.
After you're finished:
```bash
$ zeronet.py siteSign 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2
- Signing site: 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2...
Private key (input hidden):
```
* Enter the private key you got when you created the site, then:
```bash
$ zeronet.py sitePublish 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2
...
Site:13DNDk..bhC2 Publishing to 3/10 peers...
Site:13DNDk..bhC2 Successfuly published to 3 peers
- Serving files....
```
* That's it! You've successfully signed and published your modifications.
## Help keep this project alive
- Bitcoin: 1QDhxQ6PraUZa21ET5fYUCPgdrwBomnFgX
@ -167,4 +123,4 @@ Site:13DNDk..bhC2 Successfuly published to 3 peers
* More info, help, changelog, zeronet sites: https://www.reddit.com/r/zeronet/
* Come, chat with us: [#zeronet @ FreeNode](https://kiwiirc.com/client/irc.freenode.net/zeronet) or on [gitter](https://gitter.im/HelloZeroNet/ZeroNet)
* Email: hello@zeronet.io (PGP: CB9613AE)
* Email: hello@zeronet.io (PGP: [960F FF2D 6C14 5AA6 13E8 491B 5B63 BAE6 CB96 13AE](https://zeronet.io/files/tamas@zeronet.io_pub.asc))

View file

@ -3,7 +3,7 @@ import urllib.request
import struct
import socket
import bencode
import lib.bencode_open as bencode_open
from lib.subtl.subtl import UdpTrackerClient
import socks
import sockshandler
@ -133,9 +133,7 @@ class SiteAnnouncerPlugin(object):
# Decode peers
try:
peer_data = bencode.decode(response)["peers"]
if type(peer_data) is not bytes:
peer_data = peer_data.encode()
peer_data = bencode_open.loads(response)[b"peers"]
response = None
peer_count = int(len(peer_data) / 6)
peers = []

View file

@ -0,0 +1,5 @@
{
"name": "AnnounceBitTorrent",
"description": "Discover new peers using BitTorrent trackers.",
"default": "enabled"
}

View file

@ -120,12 +120,11 @@ class LocalAnnouncer(BroadcastServer.BroadcastServer):
@PluginManager.registerTo("FileServer")
class FileServerPlugin(object):
def __init__(self, *args, **kwargs):
res = super(FileServerPlugin, self).__init__(*args, **kwargs)
super(FileServerPlugin, self).__init__(*args, **kwargs)
if config.broadcast_port and config.tor != "always" and not config.disable_udp:
self.local_announcer = LocalAnnouncer(self, config.broadcast_port)
else:
self.local_announcer = None
return res
def start(self, *args, **kwargs):
if self.local_announcer:

View file

@ -0,0 +1,5 @@
{
"name": "AnnounceLocal",
"description": "Discover LAN clients using UDP broadcasting.",
"default": "enabled"
}

View file

@ -0,0 +1,5 @@
{
"name": "AnnounceShare",
"description": "Share possible trackers between clients.",
"default": "enabled"
}

View file

@ -21,6 +21,15 @@ def importHostClasses():
# Process result got back from tracker
def processPeerRes(tracker_address, site, peers):
added = 0
# Onion
found_onion = 0
for packed_address in peers["onion"]:
found_onion += 1
peer_onion, peer_port = helper.unpackOnionAddress(packed_address)
if site.addPeer(peer_onion, peer_port, source="tracker"):
added += 1
# Ip4
found_ipv4 = 0
peers_normal = itertools.chain(peers.get("ip4", []), peers.get("ipv4", []), peers.get("ipv6", []))
@ -29,13 +38,6 @@ def processPeerRes(tracker_address, site, peers):
peer_ip, peer_port = helper.unpackAddress(packed_address)
if site.addPeer(peer_ip, peer_port, source="tracker"):
added += 1
# Onion
found_onion = 0
for packed_address in peers["onion"]:
found_onion += 1
peer_onion, peer_port = helper.unpackOnionAddress(packed_address)
if site.addPeer(peer_onion, peer_port, source="tracker"):
added += 1
if added:
site.worker_manager.onPeers()

View file

@ -0,0 +1,5 @@
{
"name": "AnnounceZero",
"description": "Announce using ZeroNet protocol.",
"default": "enabled"
}

View file

@ -0,0 +1,143 @@
import os
import json
import contextlib
import time
from Plugin import PluginManager
from Config import config
@PluginManager.registerTo("Actions")
class ActionsPlugin:
def getBenchmarkTests(self, online=False):
tests = super().getBenchmarkTests(online)
tests.extend([
{"func": self.testDbConnect, "num": 10, "time_standard": 0.27},
{"func": self.testDbInsert, "num": 10, "time_standard": 0.91},
{"func": self.testDbInsertMultiuser, "num": 1, "time_standard": 0.57},
{"func": self.testDbQueryIndexed, "num": 1000, "time_standard": 0.84},
{"func": self.testDbQueryNotIndexed, "num": 1000, "time_standard": 1.30}
])
return tests
@contextlib.contextmanager
def getTestDb(self):
from Db import Db
path = "%s/benchmark.db" % config.data_dir
if os.path.isfile(path):
os.unlink(path)
schema = {
"db_name": "TestDb",
"db_file": path,
"maps": {
".*": {
"to_table": {
"test": "test"
}
}
},
"tables": {
"test": {
"cols": [
["test_id", "INTEGER"],
["title", "TEXT"],
["json_id", "INTEGER REFERENCES json (json_id)"]
],
"indexes": ["CREATE UNIQUE INDEX test_key ON test(test_id, json_id)"],
"schema_changed": 1426195822
}
}
}
db = Db.Db(schema, path)
yield db
db.close()
if os.path.isfile(path):
os.unlink(path)
def testDbConnect(self, num_run=1):
import sqlite3
for i in range(num_run):
with self.getTestDb() as db:
db.checkTables()
yield "."
yield "(SQLite version: %s, API: %s)" % (sqlite3.sqlite_version, sqlite3.version)
def testDbInsert(self, num_run=1):
yield "x 1000 lines "
for u in range(num_run):
with self.getTestDb() as db:
db.checkTables()
data = {"test": []}
for i in range(1000): # 1000 line of data
data["test"].append({"test_id": i, "title": "Testdata for %s message %s" % (u, i)})
json.dump(data, open("%s/test_%s.json" % (config.data_dir, u), "w"))
db.updateJson("%s/test_%s.json" % (config.data_dir, u))
os.unlink("%s/test_%s.json" % (config.data_dir, u))
assert db.execute("SELECT COUNT(*) FROM test").fetchone()[0] == 1000
yield "."
def fillTestDb(self, db):
db.checkTables()
cur = db.getCursor()
cur.logging = False
for u in range(100, 200): # 100 user
data = {"test": []}
for i in range(100): # 1000 line of data
data["test"].append({"test_id": i, "title": "Testdata for %s message %s" % (u, i)})
json.dump(data, open("%s/test_%s.json" % (config.data_dir, u), "w"))
db.updateJson("%s/test_%s.json" % (config.data_dir, u), cur=cur)
os.unlink("%s/test_%s.json" % (config.data_dir, u))
if u % 10 == 0:
yield "."
def testDbInsertMultiuser(self, num_run=1):
yield "x 100 users x 100 lines "
for u in range(num_run):
with self.getTestDb() as db:
for progress in self.fillTestDb(db):
yield progress
num_rows = db.execute("SELECT COUNT(*) FROM test").fetchone()[0]
assert num_rows == 10000, "%s != 10000" % num_rows
def testDbQueryIndexed(self, num_run=1):
s = time.time()
with self.getTestDb() as db:
for progress in self.fillTestDb(db):
pass
yield " (Db warmup done in %.3fs) " % (time.time() - s)
found_total = 0
for i in range(num_run): # 1000x by test_id
found = 0
res = db.execute("SELECT * FROM test WHERE test_id = %s" % (i % 100))
for row in res:
found_total += 1
found += 1
del(res)
yield "."
assert found == 100, "%s != 100 (i: %s)" % (found, i)
yield "Found: %s" % found_total
def testDbQueryNotIndexed(self, num_run=1):
s = time.time()
with self.getTestDb() as db:
for progress in self.fillTestDb(db):
pass
yield " (Db warmup done in %.3fs) " % (time.time() - s)
found_total = 0
for i in range(num_run): # 1000x by test_id
found = 0
res = db.execute("SELECT * FROM test WHERE json_id = %s" % i)
for row in res:
found_total += 1
found += 1
yield "."
del(res)
if i == 0 or i > 100:
assert found == 0, "%s != 0 (i: %s)" % (found, i)
else:
assert found == 100, "%s != 100 (i: %s)" % (found, i)
yield "Found: %s" % found_total

View file

@ -0,0 +1,183 @@
import os
import io
from collections import OrderedDict
from Plugin import PluginManager
from Config import config
from util import Msgpack
@PluginManager.registerTo("Actions")
class ActionsPlugin:
def createZipFile(self, path):
import zipfile
test_data = b"Test" * 1024
file_name = b"\xc3\x81rv\xc3\xadzt\xc5\xb1r\xc5\x91%s.txt".decode("utf8")
with zipfile.ZipFile(path, 'w') as archive:
for y in range(100):
zip_info = zipfile.ZipInfo(file_name % y, (1980, 1, 1, 0, 0, 0))
zip_info.compress_type = zipfile.ZIP_DEFLATED
zip_info.create_system = 3
zip_info.flag_bits = 0
zip_info.external_attr = 25165824
archive.writestr(zip_info, test_data)
def testPackZip(self, num_run=1):
"""
Test zip file creating
"""
yield "x 100 x 5KB "
from Crypt import CryptHash
zip_path = '%s/test.zip' % config.data_dir
for i in range(num_run):
self.createZipFile(zip_path)
yield "."
archive_size = os.path.getsize(zip_path) / 1024
yield "(Generated file size: %.2fkB)" % archive_size
hash = CryptHash.sha512sum(open(zip_path, "rb"))
valid = "cb32fb43783a1c06a2170a6bc5bb228a032b67ff7a1fd7a5efb9b467b400f553"
assert hash == valid, "Invalid hash: %s != %s<br>" % (hash, valid)
os.unlink(zip_path)
def testUnpackZip(self, num_run=1):
"""
Test zip file reading
"""
yield "x 100 x 5KB "
import zipfile
zip_path = '%s/test.zip' % config.data_dir
test_data = b"Test" * 1024
file_name = b"\xc3\x81rv\xc3\xadzt\xc5\xb1r\xc5\x91".decode("utf8")
self.createZipFile(zip_path)
for i in range(num_run):
with zipfile.ZipFile(zip_path) as archive:
for f in archive.filelist:
assert f.filename.startswith(file_name), "Invalid filename: %s != %s" % (f.filename, file_name)
data = archive.open(f.filename).read()
assert archive.open(f.filename).read() == test_data, "Invalid data: %s..." % data[0:30]
yield "."
os.unlink(zip_path)
def createArchiveFile(self, path, archive_type="gz"):
import tarfile
import gzip
# Monkey patch _init_write_gz to use fixed date in order to keep the hash independent from datetime
def nodate_write_gzip_header(self):
self._write_mtime = 0
original_write_gzip_header(self)
test_data_io = io.BytesIO(b"Test" * 1024)
file_name = b"\xc3\x81rv\xc3\xadzt\xc5\xb1r\xc5\x91%s.txt".decode("utf8")
original_write_gzip_header = gzip.GzipFile._write_gzip_header
gzip.GzipFile._write_gzip_header = nodate_write_gzip_header
with tarfile.open(path, 'w:%s' % archive_type) as archive:
for y in range(100):
test_data_io.seek(0)
tar_info = tarfile.TarInfo(file_name % y)
tar_info.size = 4 * 1024
archive.addfile(tar_info, test_data_io)
def testPackArchive(self, num_run=1, archive_type="gz"):
"""
Test creating tar archive files
"""
yield "x 100 x 5KB "
from Crypt import CryptHash
hash_valid_db = {
"gz": "92caec5121a31709cbbc8c11b0939758e670b055bbbe84f9beb3e781dfde710f",
"bz2": "b613f41e6ee947c8b9b589d3e8fa66f3e28f63be23f4faf015e2f01b5c0b032d",
"xz": "ae43892581d770959c8d993daffab25fd74490b7cf9fafc7aaee746f69895bcb",
}
archive_path = '%s/test.tar.%s' % (config.data_dir, archive_type)
for i in range(num_run):
self.createArchiveFile(archive_path, archive_type=archive_type)
yield "."
archive_size = os.path.getsize(archive_path) / 1024
yield "(Generated file size: %.2fkB)" % archive_size
hash = CryptHash.sha512sum(open("%s/test.tar.%s" % (config.data_dir, archive_type), "rb"))
valid = hash_valid_db[archive_type]
assert hash == valid, "Invalid hash: %s != %s<br>" % (hash, valid)
if os.path.isfile(archive_path):
os.unlink(archive_path)
def testUnpackArchive(self, num_run=1, archive_type="gz"):
"""
Test reading tar archive files
"""
yield "x 100 x 5KB "
import tarfile
test_data = b"Test" * 1024
file_name = b"\xc3\x81rv\xc3\xadzt\xc5\xb1r\xc5\x91%s.txt".decode("utf8")
archive_path = '%s/test.tar.%s' % (config.data_dir, archive_type)
self.createArchiveFile(archive_path, archive_type=archive_type)
for i in range(num_run):
with tarfile.open(archive_path, 'r:%s' % archive_type) as archive:
for y in range(100):
assert archive.extractfile(file_name % y).read() == test_data
yield "."
if os.path.isfile(archive_path):
os.unlink(archive_path)
def testPackMsgpack(self, num_run=1):
"""
Test msgpack encoding
"""
yield "x 100 x 5KB "
binary = b'fqv\xf0\x1a"e\x10,\xbe\x9cT\x9e(\xa5]u\x072C\x8c\x15\xa2\xa8\x93Sw)\x19\x02\xdd\t\xfb\xf67\x88\xd9\xee\x86\xa1\xe4\xb6,\xc6\x14\xbb\xd7$z\x1d\xb2\xda\x85\xf5\xa0\x97^\x01*\xaf\xd3\xb0!\xb7\x9d\xea\x89\xbbh8\xa1"\xa7]e(@\xa2\xa5g\xb7[\xae\x8eE\xc2\x9fL\xb6s\x19\x19\r\xc8\x04S\xd0N\xe4]?/\x01\xea\xf6\xec\xd1\xb3\xc2\x91\x86\xd7\xf4K\xdf\xc2lV\xf4\xe8\x80\xfc\x8ep\xbb\x82\xb3\x86\x98F\x1c\xecS\xc8\x15\xcf\xdc\xf1\xed\xfc\xd8\x18r\xf9\x80\x0f\xfa\x8cO\x97(\x0b]\xf1\xdd\r\xe7\xbf\xed\x06\xbd\x1b?\xc5\xa0\xd7a\x82\xf3\xa8\xe6@\xf3\ri\xa1\xb10\xf6\xd4W\xbc\x86\x1a\xbb\xfd\x94!bS\xdb\xaeM\x92\x00#\x0b\xf7\xad\xe9\xc2\x8e\x86\xbfi![%\xd31]\xc6\xfc2\xc9\xda\xc6v\x82P\xcc\xa9\xea\xb9\xff\xf6\xc8\x17iD\xcf\xf3\xeeI\x04\xe9\xa1\x19\xbb\x01\x92\xf5nn4K\xf8\xbb\xc6\x17e>\xa7 \xbbv'
data = OrderedDict(
sorted({"int": 1024 * 1024 * 1024, "float": 12345.67890, "text": "hello" * 1024, "binary": binary}.items())
)
data_packed_valid = b'\x84\xa6binary\xc5\x01\x00fqv\xf0\x1a"e\x10,\xbe\x9cT\x9e(\xa5]u\x072C\x8c\x15\xa2\xa8\x93Sw)\x19\x02\xdd\t\xfb\xf67\x88\xd9\xee\x86\xa1\xe4\xb6,\xc6\x14\xbb\xd7$z\x1d\xb2\xda\x85\xf5\xa0\x97^\x01*\xaf\xd3\xb0!\xb7\x9d\xea\x89\xbbh8\xa1"\xa7]e(@\xa2\xa5g\xb7[\xae\x8eE\xc2\x9fL\xb6s\x19\x19\r\xc8\x04S\xd0N\xe4]?/\x01\xea\xf6\xec\xd1\xb3\xc2\x91\x86\xd7\xf4K\xdf\xc2lV\xf4\xe8\x80\xfc\x8ep\xbb\x82\xb3\x86\x98F\x1c\xecS\xc8\x15\xcf\xdc\xf1\xed\xfc\xd8\x18r\xf9\x80\x0f\xfa\x8cO\x97(\x0b]\xf1\xdd\r\xe7\xbf\xed\x06\xbd\x1b?\xc5\xa0\xd7a\x82\xf3\xa8\xe6@\xf3\ri\xa1\xb10\xf6\xd4W\xbc\x86\x1a\xbb\xfd\x94!bS\xdb\xaeM\x92\x00#\x0b\xf7\xad\xe9\xc2\x8e\x86\xbfi![%\xd31]\xc6\xfc2\xc9\xda\xc6v\x82P\xcc\xa9\xea\xb9\xff\xf6\xc8\x17iD\xcf\xf3\xeeI\x04\xe9\xa1\x19\xbb\x01\x92\xf5nn4K\xf8\xbb\xc6\x17e>\xa7 \xbbv\xa5float\xcb@\xc8\x1c\xd6\xe61\xf8\xa1\xa3int\xce@\x00\x00\x00\xa4text\xda\x14\x00'
data_packed_valid += b'hello' * 1024
for y in range(num_run):
for i in range(100):
data_packed = Msgpack.pack(data)
yield "."
assert data_packed == data_packed_valid, "%s<br>!=<br>%s" % (repr(data_packed), repr(data_packed_valid))
def testUnpackMsgpack(self, num_run=1):
"""
Test msgpack decoding
"""
yield "x 5KB "
binary = b'fqv\xf0\x1a"e\x10,\xbe\x9cT\x9e(\xa5]u\x072C\x8c\x15\xa2\xa8\x93Sw)\x19\x02\xdd\t\xfb\xf67\x88\xd9\xee\x86\xa1\xe4\xb6,\xc6\x14\xbb\xd7$z\x1d\xb2\xda\x85\xf5\xa0\x97^\x01*\xaf\xd3\xb0!\xb7\x9d\xea\x89\xbbh8\xa1"\xa7]e(@\xa2\xa5g\xb7[\xae\x8eE\xc2\x9fL\xb6s\x19\x19\r\xc8\x04S\xd0N\xe4]?/\x01\xea\xf6\xec\xd1\xb3\xc2\x91\x86\xd7\xf4K\xdf\xc2lV\xf4\xe8\x80\xfc\x8ep\xbb\x82\xb3\x86\x98F\x1c\xecS\xc8\x15\xcf\xdc\xf1\xed\xfc\xd8\x18r\xf9\x80\x0f\xfa\x8cO\x97(\x0b]\xf1\xdd\r\xe7\xbf\xed\x06\xbd\x1b?\xc5\xa0\xd7a\x82\xf3\xa8\xe6@\xf3\ri\xa1\xb10\xf6\xd4W\xbc\x86\x1a\xbb\xfd\x94!bS\xdb\xaeM\x92\x00#\x0b\xf7\xad\xe9\xc2\x8e\x86\xbfi![%\xd31]\xc6\xfc2\xc9\xda\xc6v\x82P\xcc\xa9\xea\xb9\xff\xf6\xc8\x17iD\xcf\xf3\xeeI\x04\xe9\xa1\x19\xbb\x01\x92\xf5nn4K\xf8\xbb\xc6\x17e>\xa7 \xbbv'
data = OrderedDict(
sorted({"int": 1024 * 1024 * 1024, "float": 12345.67890, "text": "hello" * 1024, "binary": binary}.items())
)
data_packed = b'\x84\xa6binary\xc5\x01\x00fqv\xf0\x1a"e\x10,\xbe\x9cT\x9e(\xa5]u\x072C\x8c\x15\xa2\xa8\x93Sw)\x19\x02\xdd\t\xfb\xf67\x88\xd9\xee\x86\xa1\xe4\xb6,\xc6\x14\xbb\xd7$z\x1d\xb2\xda\x85\xf5\xa0\x97^\x01*\xaf\xd3\xb0!\xb7\x9d\xea\x89\xbbh8\xa1"\xa7]e(@\xa2\xa5g\xb7[\xae\x8eE\xc2\x9fL\xb6s\x19\x19\r\xc8\x04S\xd0N\xe4]?/\x01\xea\xf6\xec\xd1\xb3\xc2\x91\x86\xd7\xf4K\xdf\xc2lV\xf4\xe8\x80\xfc\x8ep\xbb\x82\xb3\x86\x98F\x1c\xecS\xc8\x15\xcf\xdc\xf1\xed\xfc\xd8\x18r\xf9\x80\x0f\xfa\x8cO\x97(\x0b]\xf1\xdd\r\xe7\xbf\xed\x06\xbd\x1b?\xc5\xa0\xd7a\x82\xf3\xa8\xe6@\xf3\ri\xa1\xb10\xf6\xd4W\xbc\x86\x1a\xbb\xfd\x94!bS\xdb\xaeM\x92\x00#\x0b\xf7\xad\xe9\xc2\x8e\x86\xbfi![%\xd31]\xc6\xfc2\xc9\xda\xc6v\x82P\xcc\xa9\xea\xb9\xff\xf6\xc8\x17iD\xcf\xf3\xeeI\x04\xe9\xa1\x19\xbb\x01\x92\xf5nn4K\xf8\xbb\xc6\x17e>\xa7 \xbbv\xa5float\xcb@\xc8\x1c\xd6\xe61\xf8\xa1\xa3int\xce@\x00\x00\x00\xa4text\xda\x14\x00'
data_packed += b'hello' * 1024
for y in range(num_run):
data_unpacked = Msgpack.unpack(data_packed, decode=False)
yield "."
assert data_unpacked == data, "%s<br>!=<br>%s" % (data_unpacked, data)
def testUnpackMsgpackStreaming(self, num_run=1, fallback=False):
"""
Test streaming msgpack decoding
"""
yield "x 1000 x 5KB "
binary = b'fqv\xf0\x1a"e\x10,\xbe\x9cT\x9e(\xa5]u\x072C\x8c\x15\xa2\xa8\x93Sw)\x19\x02\xdd\t\xfb\xf67\x88\xd9\xee\x86\xa1\xe4\xb6,\xc6\x14\xbb\xd7$z\x1d\xb2\xda\x85\xf5\xa0\x97^\x01*\xaf\xd3\xb0!\xb7\x9d\xea\x89\xbbh8\xa1"\xa7]e(@\xa2\xa5g\xb7[\xae\x8eE\xc2\x9fL\xb6s\x19\x19\r\xc8\x04S\xd0N\xe4]?/\x01\xea\xf6\xec\xd1\xb3\xc2\x91\x86\xd7\xf4K\xdf\xc2lV\xf4\xe8\x80\xfc\x8ep\xbb\x82\xb3\x86\x98F\x1c\xecS\xc8\x15\xcf\xdc\xf1\xed\xfc\xd8\x18r\xf9\x80\x0f\xfa\x8cO\x97(\x0b]\xf1\xdd\r\xe7\xbf\xed\x06\xbd\x1b?\xc5\xa0\xd7a\x82\xf3\xa8\xe6@\xf3\ri\xa1\xb10\xf6\xd4W\xbc\x86\x1a\xbb\xfd\x94!bS\xdb\xaeM\x92\x00#\x0b\xf7\xad\xe9\xc2\x8e\x86\xbfi![%\xd31]\xc6\xfc2\xc9\xda\xc6v\x82P\xcc\xa9\xea\xb9\xff\xf6\xc8\x17iD\xcf\xf3\xeeI\x04\xe9\xa1\x19\xbb\x01\x92\xf5nn4K\xf8\xbb\xc6\x17e>\xa7 \xbbv'
data = OrderedDict(
sorted({"int": 1024 * 1024 * 1024, "float": 12345.67890, "text": "hello" * 1024, "binary": binary}.items())
)
data_packed = b'\x84\xa6binary\xc5\x01\x00fqv\xf0\x1a"e\x10,\xbe\x9cT\x9e(\xa5]u\x072C\x8c\x15\xa2\xa8\x93Sw)\x19\x02\xdd\t\xfb\xf67\x88\xd9\xee\x86\xa1\xe4\xb6,\xc6\x14\xbb\xd7$z\x1d\xb2\xda\x85\xf5\xa0\x97^\x01*\xaf\xd3\xb0!\xb7\x9d\xea\x89\xbbh8\xa1"\xa7]e(@\xa2\xa5g\xb7[\xae\x8eE\xc2\x9fL\xb6s\x19\x19\r\xc8\x04S\xd0N\xe4]?/\x01\xea\xf6\xec\xd1\xb3\xc2\x91\x86\xd7\xf4K\xdf\xc2lV\xf4\xe8\x80\xfc\x8ep\xbb\x82\xb3\x86\x98F\x1c\xecS\xc8\x15\xcf\xdc\xf1\xed\xfc\xd8\x18r\xf9\x80\x0f\xfa\x8cO\x97(\x0b]\xf1\xdd\r\xe7\xbf\xed\x06\xbd\x1b?\xc5\xa0\xd7a\x82\xf3\xa8\xe6@\xf3\ri\xa1\xb10\xf6\xd4W\xbc\x86\x1a\xbb\xfd\x94!bS\xdb\xaeM\x92\x00#\x0b\xf7\xad\xe9\xc2\x8e\x86\xbfi![%\xd31]\xc6\xfc2\xc9\xda\xc6v\x82P\xcc\xa9\xea\xb9\xff\xf6\xc8\x17iD\xcf\xf3\xeeI\x04\xe9\xa1\x19\xbb\x01\x92\xf5nn4K\xf8\xbb\xc6\x17e>\xa7 \xbbv\xa5float\xcb@\xc8\x1c\xd6\xe61\xf8\xa1\xa3int\xce@\x00\x00\x00\xa4text\xda\x14\x00'
data_packed += b'hello' * 1024
for i in range(num_run):
unpacker = Msgpack.getUnpacker(decode=False, fallback=fallback)
for y in range(1000):
unpacker.feed(data_packed)
for data_unpacked in unpacker:
pass
yield "."
assert data == data_unpacked, "%s != %s" % (data_unpacked, data)

View file

@ -0,0 +1,364 @@
import os
import time
import io
import math
import hashlib
import re
import sys
from Config import config
from Crypt import CryptHash
from Plugin import PluginManager
from Debug import Debug
from util import helper
plugin_dir = os.path.dirname(__file__)
benchmark_key = None
@PluginManager.registerTo("UiRequest")
class UiRequestPlugin(object):
@helper.encodeResponse
def actionBenchmark(self):
global benchmark_key
script_nonce = self.getScriptNonce()
if not benchmark_key:
benchmark_key = CryptHash.random(encoding="base64")
self.sendHeader(script_nonce=script_nonce)
if "Multiuser" in PluginManager.plugin_manager.plugin_names and not config.multiuser_local:
yield "This function is disabled on this proxy"
return
data = self.render(
plugin_dir + "/media/benchmark.html",
script_nonce=script_nonce,
benchmark_key=benchmark_key,
filter=re.sub("[^A-Za-z0-9]", "", self.get.get("filter", ""))
)
yield data
@helper.encodeResponse
def actionBenchmarkResult(self):
global benchmark_key
if self.get.get("benchmark_key", "") != benchmark_key:
return self.error403("Invalid benchmark key")
self.sendHeader(content_type="text/plain", noscript=True)
if "Multiuser" in PluginManager.plugin_manager.plugin_names and not config.multiuser_local:
yield "This function is disabled on this proxy"
return
yield " " * 1024 # Head (required for streaming)
import main
s = time.time()
for part in main.actions.testBenchmark(filter=self.get.get("filter", "")):
yield part
yield "\n - Total time: %.3fs" % (time.time() - s)
@PluginManager.registerTo("Actions")
class ActionsPlugin:
def getMultiplerTitle(self, multipler):
if multipler < 0.3:
multipler_title = "Sloooow"
elif multipler < 0.6:
multipler_title = "Ehh"
elif multipler < 0.8:
multipler_title = "Goodish"
elif multipler < 1.2:
multipler_title = "OK"
elif multipler < 1.7:
multipler_title = "Fine"
elif multipler < 2.5:
multipler_title = "Fast"
elif multipler < 3.5:
multipler_title = "WOW"
else:
multipler_title = "Insane!!"
return multipler_title
def formatResult(self, taken, standard):
if not standard:
return " Done in %.3fs" % taken
if taken > 0:
multipler = standard / taken
else:
multipler = 99
multipler_title = self.getMultiplerTitle(multipler)
return " Done in %.3fs = %s (%.2fx)" % (taken, multipler_title, multipler)
def getBenchmarkTests(self, online=False):
if hasattr(super(), "getBenchmarkTests"):
tests = super().getBenchmarkTests(online)
else:
tests = []
tests.extend([
{"func": self.testHdPrivatekey, "num": 50, "time_standard": 0.57},
{"func": self.testSign, "num": 20, "time_standard": 0.46},
{"func": self.testVerify, "kwargs": {"lib_verify": "sslcrypto_fallback"}, "num": 20, "time_standard": 0.38},
{"func": self.testVerify, "kwargs": {"lib_verify": "sslcrypto"}, "num": 200, "time_standard": 0.30},
{"func": self.testVerify, "kwargs": {"lib_verify": "libsecp256k1"}, "num": 200, "time_standard": 0.10},
{"func": self.testPackMsgpack, "num": 100, "time_standard": 0.35},
{"func": self.testUnpackMsgpackStreaming, "kwargs": {"fallback": False}, "num": 100, "time_standard": 0.35},
{"func": self.testUnpackMsgpackStreaming, "kwargs": {"fallback": True}, "num": 10, "time_standard": 0.5},
{"func": self.testPackZip, "num": 5, "time_standard": 0.065},
{"func": self.testPackArchive, "kwargs": {"archive_type": "gz"}, "num": 5, "time_standard": 0.08},
{"func": self.testPackArchive, "kwargs": {"archive_type": "bz2"}, "num": 5, "time_standard": 0.68},
{"func": self.testPackArchive, "kwargs": {"archive_type": "xz"}, "num": 5, "time_standard": 0.47},
{"func": self.testUnpackZip, "num": 20, "time_standard": 0.25},
{"func": self.testUnpackArchive, "kwargs": {"archive_type": "gz"}, "num": 20, "time_standard": 0.28},
{"func": self.testUnpackArchive, "kwargs": {"archive_type": "bz2"}, "num": 20, "time_standard": 0.83},
{"func": self.testUnpackArchive, "kwargs": {"archive_type": "xz"}, "num": 20, "time_standard": 0.38},
{"func": self.testCryptHash, "kwargs": {"hash_type": "sha256"}, "num": 10, "time_standard": 0.50},
{"func": self.testCryptHash, "kwargs": {"hash_type": "sha512"}, "num": 10, "time_standard": 0.33},
{"func": self.testCryptHashlib, "kwargs": {"hash_type": "sha3_256"}, "num": 10, "time_standard": 0.33},
{"func": self.testCryptHashlib, "kwargs": {"hash_type": "sha3_512"}, "num": 10, "time_standard": 0.65},
{"func": self.testRandom, "num": 100, "time_standard": 0.08},
])
if online:
tests += [
{"func": self.testHttps, "num": 1, "time_standard": 2.1}
]
return tests
def testBenchmark(self, num_multipler=1, online=False, num_run=None, filter=None):
"""
Run benchmark on client functions
"""
tests = self.getBenchmarkTests(online=online)
if filter:
tests = [test for test in tests[:] if filter.lower() in test["func"].__name__.lower()]
yield "\n"
res = {}
multiplers = []
for test in tests:
s = time.time()
if num_run:
num_run_test = num_run
else:
num_run_test = math.ceil(test["num"] * num_multipler)
func = test["func"]
func_name = func.__name__
kwargs = test.get("kwargs", {})
key = "%s %s" % (func_name, kwargs)
if kwargs:
yield "* Running %s (%s) x %s " % (func_name, kwargs, num_run_test)
else:
yield "* Running %s x %s " % (func_name, num_run_test)
i = 0
try:
for progress in func(num_run_test, **kwargs):
i += 1
if num_run_test > 10:
should_print = i % (num_run_test / 10) == 0 or progress != "."
else:
should_print = True
if should_print:
if num_run_test == 1 and progress == ".":
progress = "..."
yield progress
time_taken = time.time() - s
if num_run:
time_standard = 0
else:
time_standard = test["time_standard"] * num_multipler
yield self.formatResult(time_taken, time_standard)
yield "\n"
res[key] = "ok"
multiplers.append(time_standard / max(time_taken, 0.001))
except Exception as err:
res[key] = err
yield "Failed!\n! Error: %s\n\n" % Debug.formatException(err)
if not res:
yield "! No tests found"
if config.action == "test":
sys.exit(1)
else:
num_failed = len([res_key for res_key, res_val in res.items() if res_val != "ok"])
num_success = len([res_key for res_key, res_val in res.items() if res_val != "ok"])
yield "* Result:\n"
yield " - Total: %s tests\n" % len(res)
yield " - Success: %s tests\n" % num_success
yield " - Failed: %s tests\n" % num_failed
if any(multiplers):
multipler_avg = sum(multiplers) / len(multiplers)
multipler_title = self.getMultiplerTitle(multipler_avg)
yield " - Average speed factor: %.2fx (%s)" % (multipler_avg, multipler_title)
if num_failed == 0 and config.action == "test":
sys.exit(1)
def testHttps(self, num_run=1):
"""
Test https connection with valid and invalid certs
"""
import urllib.request
import urllib.error
body = urllib.request.urlopen("https://google.com").read()
assert len(body) > 100
yield "."
badssl_urls = [
"https://expired.badssl.com/",
"https://wrong.host.badssl.com/",
"https://self-signed.badssl.com/",
"https://untrusted-root.badssl.com/"
]
for badssl_url in badssl_urls:
try:
body = urllib.request.urlopen(badssl_url).read()
https_err = None
except urllib.error.URLError as err:
https_err = err
assert https_err
yield "."
def testCryptHash(self, num_run=1, hash_type="sha256"):
"""
Test hashing functions
"""
yield "(5MB) "
from Crypt import CryptHash
hash_types = {
"sha256": {"func": CryptHash.sha256sum, "hash_valid": "8cd629d9d6aff6590da8b80782a5046d2673d5917b99d5603c3dcb4005c45ffa"},
"sha512": {"func": CryptHash.sha512sum, "hash_valid": "9ca7e855d430964d5b55b114e95c6bbb114a6d478f6485df93044d87b108904d"}
}
hash_func = hash_types[hash_type]["func"]
hash_valid = hash_types[hash_type]["hash_valid"]
data = io.BytesIO(b"Hello" * 1024 * 1024) # 5MB
for i in range(num_run):
data.seek(0)
hash = hash_func(data)
yield "."
assert hash == hash_valid, "%s != %s" % (hash, hash_valid)
def testCryptHashlib(self, num_run=1, hash_type="sha3_256"):
"""
Test SHA3 hashing functions
"""
yield "x 5MB "
hash_types = {
"sha3_256": {"func": hashlib.sha3_256, "hash_valid": "c8aeb3ef9fe5d6404871c0d2a4410a4d4e23268e06735648c9596f436c495f7e"},
"sha3_512": {"func": hashlib.sha3_512, "hash_valid": "b75dba9472d8af3cc945ce49073f3f8214d7ac12086c0453fb08944823dee1ae83b3ffbc87a53a57cc454521d6a26fe73ff0f3be38dddf3f7de5d7692ebc7f95"},
}
hash_func = hash_types[hash_type]["func"]
hash_valid = hash_types[hash_type]["hash_valid"]
data = io.BytesIO(b"Hello" * 1024 * 1024) # 5MB
for i in range(num_run):
data.seek(0)
h = hash_func()
while 1:
buff = data.read(1024 * 64)
if not buff:
break
h.update(buff)
hash = h.hexdigest()
yield "."
assert hash == hash_valid, "%s != %s" % (hash, hash_valid)
def testRandom(self, num_run=1):
"""
Test generating random data
"""
yield "x 1000 x 256 bytes "
for i in range(num_run):
data_last = None
for y in range(1000):
data = os.urandom(256)
assert data != data_last
assert len(data) == 256
data_last = data
yield "."
def testHdPrivatekey(self, num_run=2):
"""
Test generating deterministic private keys from a master seed
"""
from Crypt import CryptBitcoin
seed = "e180efa477c63b0f2757eac7b1cce781877177fe0966be62754ffd4c8592ce38"
privatekeys = []
for i in range(num_run):
privatekeys.append(CryptBitcoin.hdPrivatekey(seed, i * 10))
yield "."
valid = "5JSbeF5PevdrsYjunqpg7kAGbnCVYa1T4APSL3QRu8EoAmXRc7Y"
assert privatekeys[0] == valid, "%s != %s" % (privatekeys[0], valid)
if len(privatekeys) > 1:
assert privatekeys[0] != privatekeys[-1]
def testSign(self, num_run=1):
"""
Test signing data using a private key
"""
from Crypt import CryptBitcoin
data = "Hello" * 1024
privatekey = "5JsunC55XGVqFQj5kPGK4MWgTL26jKbnPhjnmchSNPo75XXCwtk"
for i in range(num_run):
yield "."
sign = CryptBitcoin.sign(data, privatekey)
valid = "G1GXaDauZ8vX/N9Jn+MRiGm9h+I94zUhDnNYFaqMGuOiBHB+kp4cRPZOL7l1yqK5BHa6J+W97bMjvTXtxzljp6w="
assert sign == valid, "%s != %s" % (sign, valid)
def testVerify(self, num_run=1, lib_verify="btctools"):
"""
Test verification of generated signatures
"""
from Crypt import CryptBitcoin
CryptBitcoin.loadLib(lib_verify, silent=True)
data = "Hello" * 1024
privatekey = "5JsunC55XGVqFQj5kPGK4MWgTL26jKbnPhjnmchSNPo75XXCwtk"
address = CryptBitcoin.privatekeyToAddress(privatekey)
sign = "G1GXaDauZ8vX/N9Jn+MRiGm9h+I94zUhDnNYFaqMGuOiBHB+kp4cRPZOL7l1yqK5BHa6J+W97bMjvTXtxzljp6w="
for i in range(num_run):
ok = CryptBitcoin.verify(data, address, sign, lib_verify=lib_verify)
yield "."
assert ok, "does not verify from %s" % address
def testAll(self):
"""
Run all tests to check system compatibility with ZeroNet functions
"""
for progress in self.testBenchmark(online=not config.offline, num_run=1):
yield progress
@PluginManager.registerTo("ConfigPlugin")
class ConfigPlugin(object):
def createArguments(self):
back = super(ConfigPlugin, self).createArguments()
if self.getCmdlineValue("test") == "benchmark":
self.test_parser.add_argument(
'--num_multipler', help='Benchmark run time multipler',
default=1.0, type=float, metavar='num'
)
self.test_parser.add_argument(
'--filter', help='Filter running benchmark',
default=None, metavar='test name'
)
return back

View file

@ -0,0 +1,3 @@
from . import BenchmarkPlugin
from . import BenchmarkDb
from . import BenchmarkPack

View file

@ -0,0 +1,123 @@
<html>
<script nonce="{script_nonce}">
window.benchmark_key = "{benchmark_key}";
function setState(elem, text) {
var formatted = text
var parts = text.match(/\* Running (.*?)(\n|$)/g)
if (parts) {
for (var i=0; i < parts.length; i++) {
part = parts[i];
var details = part.match(/\* Running (.*?) (\.+|$)(.*)/);
if (details) {
var title = details[1]
var progress = details[2]
var result = details[3]
result_parts = result.match(/(.*) Done in ([0-9\.]+)s = (.*?) \(([0-9\.]+)x\)/)
var percent = Math.min(100, progress.length * 10)
if (result_parts) percent = 100
var style = "background-image: linear-gradient(90deg, #FFF " + percent + "%, #FFF 0%, #d9d5de 0%);"
var part_formatted = "<div class='test' style='" + style + "'>"
part_formatted += "<span class='title'>" + title + "</span><span class='percent percent-" + percent + "'>" + percent + "%</span> "
if (result_parts) {
var result_extra = result_parts[1]
var taken = result_parts[2]
var multipler_title = result_parts[3]
var multipler = result_parts[4]
part_formatted += "<div class='result result-" + multipler_title.replace(/[^A-Za-z]/g, "") + "'>"
part_formatted += " <span class='taken'>" + taken + "s</span>"
part_formatted += " <span class='multipler'>" + multipler + "x</span>"
part_formatted += " <span class='multipler-title'>" + multipler_title + "</span>"
part_formatted += "</div>"
} else {
part_formatted += "<div class='result'>" + result + "</div>"
}
part_formatted += "</div>"
formatted = formatted.replace(part, part_formatted);
}
}
}
formatted = formatted.replace(/(\! Error:.*)/, "<div class='test error'>$1</div>");
formatted = formatted.replace(/(\* Result:[^]*)/, "<div class='test summary'>$1</div>");
var is_bottom = document.body.scrollTop + document.body.clientHeight >= document.body.scrollHeight - 5;
elem.innerHTML = formatted.trim();
if (is_bottom)
document.body.scrollTop = document.body.scrollHeight;
}
function stream(url, elem) {
document.getElementById("h1").innerText = "Benchmark: Starting..."
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.setRequestHeader('Accept', 'text/html');
xhr.send(null);
xhr.onreadystatechange = function(state) {
document.getElementById("h1").innerText = "Benchmark: Running..."
setState(elem, xhr.responseText);
if (xhr.readyState == 4) {
document.getElementById("h1").innerText = "Benchmark: Done."
}
}
}
</script>
<body>
<style>
body {
background-color: #3c3546;
background-image: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23cfcfcf' fill-opacity='0.09'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");}
h1 {
font-family: monospace; color: white; font-weight: normal; text-transform: uppercase;
max-width: 690px; margin: 30px auto; margin-bottom: 10px;
}
#out {
white-space: pre-line; background-color: #ffffff1a; padding: 20px; font-family: Consolas, monospace;
font-size: 11px; width: 90%; margin: auto; max-width: 650px; box-shadow: 0px 10px 30px -10px #5c5c5c6b;
}
.test { padding: 12px; box-shadow: 0px 5px 13px -5px #5c5c5c6b; margin-bottom: -2px; background-color: white; border: 1px solid #dbdbdb; }
.test .percent { float: right; }
.test .percent-100 { display: none; }
.test .result { float: right; }
.test .title { max-width: calc(100% - 150px); display: inline-block; }
.test .multipler-title { display: inline-block; width: 50px; text-align: right; }
.test:last-child { margin-bottom: 15px; border-color: #c1c1c1; }
.test .result-Sloooow { color: red; }
.test .result-Ehh { color: #ad1457; }
.test .result-Goodish { color: #ef6c00; }
.test .result-Ok { color: #00cf03; }
.test .result-Fine { color: #00bcd4; }
.test .result-Fast { color: #4b78ff; }
.test .result-WOW { color: #9c27b0; }
.test .result-Insane { color: #d603f4; }
.test.summary { margin-top: 20px; text-transform: uppercase; border-left: 10px solid #00ff63; border-color: #00ff63; }
.test.error { background-color: #ff2259; color: white; border-color: red; }
#start { text-align: center }
.button {
background-color: white; padding: 10px 20px; display: inline-block; border-radius: 5px;
text-decoration: none; color: #673AB7; text-transform: uppercase; margin-bottom: 11px; border-bottom: 2px solid #c1bff8;
}
.button:hover { border-bottom-color: #673AB7; }
.button:active { transform: translateY(1px) }
small { text-transform: uppercase; opacity: 0.7; color: white; letter-spacing: 1px; }
</style>
<h1 id="h1">Benchmark</h1>
<div id="out">
<div id="start">
<a href="#Start" class="button" id="start_button">Start benchmark</a>
<small>(It will take around 20 sec)</small>
</div>
</div>
<script nonce="{script_nonce}">
function start() {
stream("/BenchmarkResult?benchmark_key={benchmark_key}&filter={filter}", document.getElementById("out"));
return false;
}
document.getElementById("start_button").onclick = start
</script>
</body>
</html>

View file

@ -0,0 +1,5 @@
{
"name": "Benchmark",
"description": "Test and benchmark database and cryptographic functions related to ZeroNet.",
"default": "enabled"
}

View file

@ -21,6 +21,7 @@ with warnings.catch_warnings():
from util import helper
from util import Msgpack
from util.Flag import flag
import util
from .BigfilePiecefield import BigfilePiecefield, BigfilePiecefieldPacked
@ -32,6 +33,7 @@ def importPluginnedClasses():
from Content.ContentManager import VerifyError
from Config import config
if "upload_nonces" not in locals():
upload_nonces = {}
@ -59,13 +61,44 @@ class UiRequestPlugin(object):
})
self.readMultipartHeaders(self.env['wsgi.input']) # Skip http headers
result = self.handleBigfileUpload(upload_info, self.env['wsgi.input'].read)
return json.dumps(result)
def actionBigfileUploadWebsocket(self):
ws = self.env.get("wsgi.websocket")
if not ws:
self.start_response("400 Bad Request", [])
return [b"Not a websocket request!"]
nonce = self.get.get("upload_nonce")
if nonce not in upload_nonces:
return self.error403("Upload nonce error.")
upload_info = upload_nonces[nonce]
del upload_nonces[nonce]
ws.send("poll")
buffer = b""
def read(size):
nonlocal buffer
while len(buffer) < size:
buffer += ws.receive()
ws.send("poll")
part, buffer = buffer[:size], buffer[size:]
return part
result = self.handleBigfileUpload(upload_info, read)
ws.send(json.dumps(result))
def handleBigfileUpload(self, upload_info, read):
site = upload_info["site"]
inner_path = upload_info["inner_path"]
with site.storage.open(inner_path, "wb", create_dirs=True) as out_file:
merkle_root, piece_size, piecemap_info = site.content_manager.hashBigfile(
self.env['wsgi.input'], upload_info["size"], upload_info["piece_size"], out_file
read, upload_info["size"], upload_info["piece_size"], out_file
)
if len(piecemap_info["sha512_pieces"]) == 1: # Small file, don't split
@ -104,12 +137,12 @@ class UiRequestPlugin(object):
site.content_manager.contents.loadItem(file_info["content_inner_path"]) # reload cache
return json.dumps({
return {
"merkle_root": merkle_root,
"piece_num": len(piecemap_info["sha512_pieces"]),
"piece_size": piece_size,
"inner_path": inner_path
})
}
def readMultipartHeaders(self, wsgi_input):
found = False
@ -136,7 +169,7 @@ class UiRequestPlugin(object):
@PluginManager.registerTo("UiWebsocket")
class UiWebsocketPlugin(object):
def actionBigfileUploadInit(self, to, inner_path, size):
def actionBigfileUploadInit(self, to, inner_path, size, protocol="xhr"):
valid_signers = self.site.content_manager.getValidSigners(inner_path)
auth_address = self.user.getAuthAddress(self.site.address)
if not self.site.settings["own"] and auth_address not in valid_signers:
@ -160,13 +193,31 @@ class UiWebsocketPlugin(object):
"piece_size": piece_size,
"piecemap": inner_path + ".piecemap.msgpack"
}
if protocol == "xhr":
return {
"url": "/ZeroNet-Internal/BigfileUpload?upload_nonce=" + nonce,
"piece_size": piece_size,
"inner_path": inner_path,
"file_relative_path": file_relative_path
}
elif protocol == "websocket":
server_url = self.request.getWsServerUrl()
if server_url:
proto, host = server_url.split("://")
origin = proto.replace("http", "ws") + "://" + host
else:
origin = "{origin}"
return {
"url": origin + "/ZeroNet-Internal/BigfileUploadWebsocket?upload_nonce=" + nonce,
"piece_size": piece_size,
"inner_path": inner_path,
"file_relative_path": file_relative_path
}
else:
return {"error": "Unknown protocol"}
@flag.no_multiuser
def actionSiteSetAutodownloadBigfileLimit(self, to, limit):
permissions = self.getPermissions(to)
if "ADMIN" not in permissions:
@ -207,14 +258,14 @@ class ContentManagerPlugin(object):
file_info = super(ContentManagerPlugin, self).getFileInfo(inner_path, *args, **kwargs)
return file_info
def readFile(self, file_in, size, buff_size=1024 * 64):
def readFile(self, read_func, size, buff_size=1024 * 64):
part_num = 0
recv_left = size
while 1:
part_num += 1
read_size = min(buff_size, recv_left)
part = file_in.read(read_size)
part = read_func(read_size)
if not part:
break
@ -227,7 +278,7 @@ class ContentManagerPlugin(object):
if recv_left <= 0:
break
def hashBigfile(self, file_in, size, piece_size=1024 * 1024, file_out=None):
def hashBigfile(self, read_func, size, piece_size=1024 * 1024, file_out=None):
self.site.settings["has_bigfile"] = True
recv = 0
@ -240,7 +291,7 @@ class ContentManagerPlugin(object):
mt.hash_function = CryptHash.sha512t
part = ""
for part in self.readFile(file_in, size):
for part in self.readFile(read_func, size):
if file_out:
file_out.write(part)
@ -306,7 +357,7 @@ class ContentManagerPlugin(object):
return super(ContentManagerPlugin, self).hashFile(dir_inner_path, file_relative_path, optional)
self.log.info("- [HASHING] %s" % file_relative_path)
merkle_root, piece_size, piecemap_info = self.hashBigfile(self.site.storage.open(inner_path, "rb"), file_size)
merkle_root, piece_size, piecemap_info = self.hashBigfile(self.site.storage.open(inner_path, "rb").read, file_size)
if not hash:
hash = merkle_root
@ -338,7 +389,11 @@ class ContentManagerPlugin(object):
return piecemap
def verifyPiece(self, inner_path, pos, piece):
try:
piecemap = self.getPiecemap(inner_path)
except OSError as err:
raise VerifyError("Unable to download piecemap: %s" % err)
piece_i = int(pos / piecemap["piece_size"])
if CryptHash.sha512sum(piece, format="digest") != piecemap["sha512_pieces"][piece_i]:
raise VerifyError("Invalid hash")
@ -404,9 +459,7 @@ class SiteStoragePlugin(object):
def createSparseFile(self, inner_path, size, sha512=None):
file_path = self.getPath(inner_path)
file_dir = os.path.dirname(file_path)
if not os.path.isdir(file_dir):
os.makedirs(file_dir)
self.ensureDir(os.path.dirname(inner_path))
f = open(file_path, 'wb')
f.truncate(min(1024 * 1024 * 5, size)) # Only pre-allocate up to 5MB
@ -430,9 +483,7 @@ class SiteStoragePlugin(object):
file_path = self.getPath(inner_path)
# Create dir if not exist
file_dir = os.path.dirname(file_path)
if not os.path.isdir(file_dir):
os.makedirs(file_dir)
self.ensureDir(os.path.dirname(inner_path))
if not os.path.isfile(file_path):
file_info = self.site.content_manager.getFileInfo(inner_path)
@ -751,6 +802,11 @@ class SitePlugin(object):
inner_path = inner_path.replace("|all", "")
file_info = self.needFileInfo(inner_path)
# Use default function to download non-optional file
if "piece_size" not in file_info:
return super(SitePlugin, self).needFile(inner_path, *args, **kwargs)
file_size = file_info["size"]
piece_size = file_info["piece_size"]

View file

@ -134,7 +134,7 @@ class TestBigfile:
peer_client = site_temp.addPeer(file_server.ip, 1544)
# Download site
site_temp.download(blind_includes=True).join(timeout=5)
site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10)
bad_files = site_temp.storage.verifyFiles(quick_check=True)["bad_files"]
assert not bad_files
@ -172,7 +172,7 @@ class TestBigfile:
site_temp.addPeer(file_server.ip, 1544)
# Download site
site_temp.download(blind_includes=True).join(timeout=5)
site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10)
# Open virtual file
assert not site_temp.storage.isFile(inner_path)
@ -255,7 +255,7 @@ class TestBigfile:
site_temp.addPeer(file_server.ip, 1544)
# Download site
site_temp.download(blind_includes=True).join(timeout=5)
site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10)
# Download second block
with site_temp.storage.openBigfile(inner_path) as f:
@ -380,7 +380,7 @@ class TestBigfile:
site_temp.addPeer(file_server.ip, 1544)
# Download site
site_temp.download(blind_includes=True).join(timeout=5)
site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10)
# Open virtual file
assert not site_temp.storage.isFile(inner_path)
@ -417,7 +417,7 @@ class TestBigfile:
site_temp.addPeer(file_server.ip, 1544)
# Download site
site_temp.download(blind_includes=True).join(timeout=5)
site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10)
# Open virtual file
assert not site_temp.storage.isFile(inner_path)
@ -453,7 +453,7 @@ class TestBigfile:
site_temp.addPeer(file_server.ip, 1544)
# Download site
site_temp.download(blind_includes=True).join(timeout=5)
site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10)
# Open virtual file
assert not site_temp.storage.isFile(inner_path)
@ -482,7 +482,7 @@ class TestBigfile:
site_temp.addPeer(file_server.ip, 1544)
# Download site
site_temp.download(blind_includes=True).join(timeout=5)
site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10)
# Open virtual file
assert not site_temp.storage.isFile(inner_path)
@ -507,7 +507,7 @@ class TestBigfile:
site_temp.addPeer(file_server.ip, 1544)
# Download site
site_temp.download(blind_includes=True).join(timeout=5)
site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10)
with Spy.Spy(FileRequest, "route") as requests:
site_temp.needFile("%s|%s-%s" % (inner_path, 0, 1 * self.piece_size))
@ -529,7 +529,7 @@ class TestBigfile:
with Spy.Spy(FileRequest, "route") as requests:
site.publish()
time.sleep(0.1)
site_temp.download(blind_includes=True).join(timeout=5) # Wait for download
site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10) # Wait for download
assert len([req[1] for req in requests if req[1] == "streamFile"]) == 0
@ -563,7 +563,7 @@ class TestBigfile:
site_temp.addPeer(file_server.ip, 1544)
# Download site
site_temp.download(blind_includes=True).join(timeout=5)
site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10)
if "piecemap" in site.content_manager.getFileInfo(inner_path): # Bigfile
site_temp.needFile(inner_path + "|all")

View file

@ -119,6 +119,8 @@ class ChartCollector(object):
value = collector(peers)
else:
value = collector()
except ValueError:
value = None
except Exception as err:
self.log.info("Collector %s error: %s" % (key, err))
value = None
@ -146,15 +148,14 @@ class ChartCollector(object):
s = time.time()
cur = self.db.getCursor()
cur.cursor.executemany("INSERT INTO data (type_id, value, date_added) VALUES (?, ?, ?)", values)
cur.close()
cur.executemany("INSERT INTO data (type_id, value, date_added) VALUES (?, ?, ?)", values)
self.log.debug("Global collectors inserted in %.3fs" % (time.time() - s))
def collectSites(self, sites, collectors, last_values):
now = int(time.time())
s = time.time()
values = []
for address, site in sites.items():
for address, site in list(sites.items()):
site_datas = self.collectDatas(collectors, last_values["site:%s" % address], site)
for key, value in site_datas.items():
values.append((self.db.getTypeId(key), self.db.getSiteId(address), value, now))
@ -163,8 +164,7 @@ class ChartCollector(object):
s = time.time()
cur = self.db.getCursor()
cur.cursor.executemany("INSERT INTO data (type_id, site_id, value, date_added) VALUES (?, ?, ?, ?)", values)
cur.close()
cur.executemany("INSERT INTO data (type_id, site_id, value, date_added) VALUES (?, ?, ?, ?)", values)
self.log.debug("Site collectors inserted in %.3fs" % (time.time() - s))
def collector(self):

View file

@ -48,15 +48,15 @@ class ChartDb(Db):
def getTypeId(self, name):
if name not in self.types:
self.execute("INSERT INTO type ?", {"name": name})
self.types[name] = self.cur.cursor.lastrowid
res = self.execute("INSERT INTO type ?", {"name": name})
self.types[name] = res.lastrowid
return self.types[name]
def getSiteId(self, address):
if address not in self.sites:
self.execute("INSERT INTO site ?", {"address": address})
self.sites[address] = self.cur.cursor.lastrowid
res = self.execute("INSERT INTO site ?", {"address": address})
self.sites[address] = res.lastrowid
return self.sites[address]

View file

@ -5,6 +5,7 @@ import gevent
from Config import config
from util import helper
from util.Flag import flag
from Plugin import PluginManager
from .ChartDb import ChartDb
from .ChartCollector import ChartCollector
@ -28,10 +29,8 @@ class SiteManagerPlugin(object):
@PluginManager.registerTo("UiWebsocket")
class UiWebsocketPlugin(object):
@flag.admin
def actionChartDbQuery(self, to, query, params=None):
if not "ADMIN" in self.permissions:
return {"error": "No permission"}
if config.debug or config.verbose:
s = time.time()
rows = []
@ -49,10 +48,8 @@ class UiWebsocketPlugin(object):
self.log.debug("Slow query: %s (%.3fs)" % (query, time.time() - s))
return rows
@flag.admin
def actionChartGetPeerLocations(self, to):
if not "ADMIN" in self.permissions:
return {"error": "No permission"}
peers = {}
for site in self.server.sites.values():
peers.update(site.peers)

View file

@ -0,0 +1,5 @@
{
"name": "Chart",
"description": "Collect and provide stats of client information.",
"default": "enabled"
}

View file

@ -1,17 +1,20 @@
import time
import re
import html
import hashlib
import os
from Plugin import PluginManager
from Translate import Translate
from Config import config
from util.Flag import flag
from .ContentFilterStorage import ContentFilterStorage
plugin_dir = os.path.dirname(__file__)
if "_" not in locals():
_ = Translate("plugins/ContentFilter/languages/")
_ = Translate(plugin_dir + "/languages/")
@PluginManager.registerTo("SiteManager")
@ -21,6 +24,24 @@ class SiteManagerPlugin(object):
super(SiteManagerPlugin, self).load(*args, **kwargs)
filter_storage = ContentFilterStorage(site_manager=self)
def add(self, address, *args, **kwargs):
should_ignore_block = kwargs.get("ignore_block") or kwargs.get("settings")
if should_ignore_block:
block_details = None
elif filter_storage.isSiteblocked(address):
block_details = filter_storage.getSiteblockDetails(address)
else:
address_hashed = filter_storage.getSiteAddressHashed(address)
if filter_storage.isSiteblocked(address_hashed):
block_details = filter_storage.getSiteblockDetails(address_hashed)
else:
block_details = None
if block_details:
raise Exception("Site blocked: %s" % html.escape(block_details.get("reason", "unknown reason")))
else:
return super(SiteManagerPlugin, self).add(address, *args, **kwargs)
@PluginManager.registerTo("UiWebsocket")
class UiWebsocketPlugin(object):
@ -33,6 +54,7 @@ class UiWebsocketPlugin(object):
filter_storage.changeDbs(auth_address, "remove")
self.response(to, "ok")
@flag.no_multiuser
def actionMuteAdd(self, to, auth_address, cert_user_id, reason):
if "ADMIN" in self.getPermissions(to):
self.cbMuteAdd(to, auth_address, cert_user_id, reason)
@ -43,50 +65,73 @@ class UiWebsocketPlugin(object):
lambda res: self.cbMuteAdd(to, auth_address, cert_user_id, reason)
)
@flag.no_multiuser
def cbMuteRemove(self, to, auth_address):
del filter_storage.file_content["mutes"][auth_address]
filter_storage.save()
filter_storage.changeDbs(auth_address, "load")
self.response(to, "ok")
@flag.no_multiuser
def actionMuteRemove(self, to, auth_address):
if "ADMIN" in self.getPermissions(to):
self.cbMuteRemove(to, auth_address)
else:
cert_user_id = html.escape(filter_storage.file_content["mutes"][auth_address]["cert_user_id"])
self.cmd(
"confirm",
[_["Unmute <b>%s</b>?"] % html.escape(filter_storage.file_content["mutes"][auth_address]["cert_user_id"]), _["Unmute"]],
[_["Unmute <b>%s</b>?"] % cert_user_id, _["Unmute"]],
lambda res: self.cbMuteRemove(to, auth_address)
)
@flag.admin
def actionMuteList(self, to):
if "ADMIN" in self.getPermissions(to):
self.response(to, filter_storage.file_content["mutes"])
else:
return self.response(to, {"error": "Forbidden: Only ADMIN sites can list mutes"})
# Siteblock
@flag.no_multiuser
@flag.admin
def actionSiteblockIgnoreAddSite(self, to, site_address):
if site_address in filter_storage.site_manager.sites:
return {"error": "Site already added"}
else:
if filter_storage.site_manager.need(site_address, ignore_block=True):
return "ok"
else:
return {"error": "Invalid address"}
@flag.no_multiuser
@flag.admin
def actionSiteblockAdd(self, to, site_address, reason=None):
if "ADMIN" not in self.getPermissions(to):
return self.response(to, {"error": "Forbidden: Only ADMIN sites can add to blocklist"})
filter_storage.file_content["siteblocks"][site_address] = {"date_added": time.time(), "reason": reason}
filter_storage.save()
self.response(to, "ok")
@flag.no_multiuser
@flag.admin
def actionSiteblockRemove(self, to, site_address):
if "ADMIN" not in self.getPermissions(to):
return self.response(to, {"error": "Forbidden: Only ADMIN sites can remove from blocklist"})
del filter_storage.file_content["siteblocks"][site_address]
filter_storage.save()
self.response(to, "ok")
@flag.admin
def actionSiteblockList(self, to):
if "ADMIN" in self.getPermissions(to):
self.response(to, filter_storage.file_content["siteblocks"])
@flag.admin
def actionSiteblockGet(self, to, site_address):
if filter_storage.isSiteblocked(site_address):
res = filter_storage.getSiteblockDetails(site_address)
else:
return self.response(to, {"error": "Forbidden: Only ADMIN sites can list blocklists"})
site_address_hashed = filter_storage.getSiteAddressHashed(site_address)
if filter_storage.isSiteblocked(site_address_hashed):
res = filter_storage.getSiteblockDetails(site_address_hashed)
else:
res = {"error": "Site block not found"}
self.response(to, res)
# Include
@flag.no_multiuser
def actionFilterIncludeAdd(self, to, inner_path, description=None, address=None):
if address:
if "ADMIN" not in self.getPermissions(to):
@ -118,6 +163,7 @@ class UiWebsocketPlugin(object):
filter_storage.includeAdd(address, inner_path, description)
self.response(to, "ok")
@flag.no_multiuser
def actionFilterIncludeRemove(self, to, inner_path, address=None):
if address:
if "ADMIN" not in self.getPermissions(to):
@ -177,7 +223,7 @@ class SiteStoragePlugin(object):
@PluginManager.registerTo("UiRequest")
class UiRequestPlugin(object):
def actionWrapper(self, path, extra_headers=None):
match = re.match("/(?P<address>[A-Za-z0-9\._-]+)(?P<inner_path>/.*|$)", path)
match = re.match(r"/(?P<address>[A-Za-z0-9\._-]+)(?P<inner_path>/.*|$)", path)
if not match:
return False
address = match.group("address")
@ -185,15 +231,15 @@ class UiRequestPlugin(object):
if self.server.site_manager.get(address): # Site already exists
return super(UiRequestPlugin, self).actionWrapper(path, extra_headers)
if self.server.site_manager.isDomain(address):
address = self.server.site_manager.resolveDomain(address)
if self.isDomain(address):
address = self.resolveDomain(address)
if address:
address_sha256 = "0x" + hashlib.sha256(address.encode("utf8")).hexdigest()
address_hashed = filter_storage.getSiteAddressHashed(address)
else:
address_sha256 = None
address_hashed = None
if filter_storage.isSiteblocked(address) or filter_storage.isSiteblocked(address_sha256):
if filter_storage.isSiteblocked(address) or filter_storage.isSiteblocked(address_hashed):
site = self.server.site_manager.get(config.homepage)
if not extra_headers:
extra_headers = {}
@ -210,7 +256,7 @@ class UiRequestPlugin(object):
def actionUiMedia(self, path, *args, **kwargs):
if path.startswith("/uimedia/plugins/contentfilter/"):
file_path = path.replace("/uimedia/plugins/contentfilter/", "plugins/ContentFilter/media/")
file_path = path.replace("/uimedia/plugins/contentfilter/", plugin_dir + "/media/")
return self.actionFile(file_path)
else:
return super(UiRequestPlugin, self).actionUiMedia(path)

View file

@ -3,12 +3,14 @@ import json
import logging
import collections
import time
import hashlib
from Debug import Debug
from Plugin import PluginManager
from Config import config
from util import helper
class ContentFilterStorage(object):
def __init__(self, site_manager):
self.log = logging.getLogger("ContentFilterStorage")
@ -114,12 +116,34 @@ class ContentFilterStorage(object):
else:
return False
def getSiteAddressHashed(self, address):
return "0x" + hashlib.sha256(address.encode("ascii")).hexdigest()
def isSiteblocked(self, address):
if address in self.file_content["siteblocks"] or address in self.include_filters["siteblocks"]:
return True
else:
return False
def getSiteblockDetails(self, address):
details = self.file_content["siteblocks"].get(address)
if not details:
address_sha256 = self.getSiteAddressHashed(address)
details = self.file_content["siteblocks"].get(address_sha256)
if not details:
includes = self.file_content.get("includes", {}).values()
for include in includes:
include_site = self.site_manager.get(include["address"])
if not include_site:
continue
content = include_site.storage.loadJson(include["inner_path"])
details = content.get("siteblocks").get(address)
if details:
details["include"] = include
break
return details
# Search and remove or readd files of an user
def changeDbs(self, auth_address, action):
self.log.debug("Mute action %s on user %s" % (action, auth_address))

View file

@ -62,25 +62,7 @@ class Page extends ZeroFrame {
}
async updateSiteblockDetails(address) {
var address_sha256 = await sha256hex(address)
var blocks = await this.cmdp("siteblockList")
if (blocks[address] || blocks[address_sha256]) {
block = blocks[address]
} else {
var includes = await this.cmdp("filterIncludeList", {all_sites: true, filters: true})
for (let include of includes) {
if (include["siteblocks"][address]) {
var block = include["siteblocks"][address]
block["include"] = include
}
if (include["siteblocks"][address_sha256]) {
var block = include["siteblocks"][address_sha256]
block["include"] = include
}
}
}
this.blocks = blocks
var block = await this.cmdp("siteblockGet", address)
var reason = block["reason"]
if (!reason) reason = "Unknown reason"
var date = new Date(block["date_added"] * 1000)
@ -95,7 +77,7 @@ class Page extends ZeroFrame {
document.getElementById("visit").style.opacity = "1"
document.getElementById("visit").onclick = () => {
if (block["include"])
this.cmd("siteAdd", address, () => { this.cmd("wrapperReload") })
this.cmd("siteblockIgnoreAddSite", address, () => { this.cmd("wrapperReload") })
else
this.cmd("siteblockRemove", address, () => { this.cmd("wrapperReload") })
}

View file

@ -0,0 +1,5 @@
{
"name": "ContentFilter",
"description": "Manage site and user block list.",
"default": "enabled"
}

View file

@ -1,11 +1,16 @@
import re
import html
import copy
import os
from Plugin import PluginManager
from Translate import Translate
plugin_dir = os.path.dirname(__file__)
if "_" not in locals():
_ = Translate("plugins/Cors/languages/")
_ = Translate(plugin_dir + "/languages/")
def getCorsPath(site, inner_path):
@ -27,7 +32,11 @@ class UiWebsocketPlugin(object):
if super(UiWebsocketPlugin, self).hasSitePermission(address, cmd=cmd):
return True
if not "Cors:%s" % address in self.site.settings["permissions"] or cmd not in ["fileGet", "fileList", "dirList", "fileRules", "optionalFileInfo", "fileQuery", "dbQuery", "userGetSettings", "siteInfo"]:
allowed_commands = [
"fileGet", "fileList", "dirList", "fileRules", "optionalFileInfo",
"fileQuery", "dbQuery", "userGetSettings", "siteInfo"
]
if not "Cors:%s" % address in self.site.settings["permissions"] or cmd not in allowed_commands:
return False
else:
return True
@ -99,6 +108,6 @@ class UiRequestPlugin(object):
site = self.server.sites[path_parts["address"]]
try:
path_parts["address"], path_parts["inner_path"] = getCorsPath(site, path_parts["inner_path"])
except:
except Exception:
return None
return path_parts

View file

@ -0,0 +1,5 @@
{
"name": "Cors",
"description": "Cross site resource read.",
"default": "enabled"
}

View file

@ -1,59 +1,58 @@
import hashlib
import base64
import lib.pybitcointools as btctools
ecc_cache = {}
import struct
from lib import sslcrypto
from Crypt import Crypt
def eciesEncrypt(data, pubkey, ephemcurve=None, ciphername='aes-256-cbc'):
import pyelliptic
pubkey_openssl = toOpensslPublickey(base64.b64decode(pubkey))
curve, pubkey_x, pubkey_y, i = pyelliptic.ECC._decode_pubkey(pubkey_openssl)
if ephemcurve is None:
ephemcurve = curve
ephem = pyelliptic.ECC(curve=ephemcurve)
key = hashlib.sha512(ephem.raw_get_ecdh_key(pubkey_x, pubkey_y)).digest()
key_e, key_m = key[:32], key[32:]
pubkey = ephem.get_pubkey()
iv = pyelliptic.OpenSSL.rand(pyelliptic.OpenSSL.get_cipher(ciphername).get_blocksize())
ctx = pyelliptic.Cipher(key_e, iv, 1, ciphername)
ciphertext = iv + pubkey + ctx.ciphering(data)
mac = pyelliptic.hmac_sha256(key_m, ciphertext)
return key_e, ciphertext + mac
curve = sslcrypto.ecc.get_curve("secp256k1")
def eciesEncrypt(data, pubkey, ciphername="aes-256-cbc"):
ciphertext, key_e = curve.encrypt(
data,
base64.b64decode(pubkey),
algo=ciphername,
derivation="sha512",
return_aes_key=True
)
return key_e, ciphertext
@Crypt.thread_pool_crypt.wrap
def eciesDecryptMulti(encrypted_datas, privatekey):
texts = [] # Decoded texts
for encrypted_data in encrypted_datas:
try:
text = eciesDecrypt(encrypted_data, privatekey).decode("utf8")
texts.append(text)
except Exception:
texts.append(None)
return texts
def eciesDecrypt(ciphertext, privatekey):
return curve.decrypt(base64.b64decode(ciphertext), curve.wif_to_private(privatekey), derivation="sha512")
def decodePubkey(pubkey):
i = 0
curve = struct.unpack('!H', pubkey[i:i + 2])[0]
i += 2
tmplen = struct.unpack('!H', pubkey[i:i + 2])[0]
i += 2
pubkey_x = pubkey[i:i + tmplen]
i += tmplen
tmplen = struct.unpack('!H', pubkey[i:i + 2])[0]
i += 2
pubkey_y = pubkey[i:i + tmplen]
i += tmplen
return curve, pubkey_x, pubkey_y, i
def eciesDecrypt(encrypted_data, privatekey):
ecc_key = getEcc(privatekey)
return ecc_key.decrypt(base64.b64decode(encrypted_data))
def split(encrypted):
iv = encrypted[0:16]
ciphertext = encrypted[16 + 70:-32]
curve, pubkey_x, pubkey_y, i = decodePubkey(encrypted[16:])
ciphertext = encrypted[16 + i:-32]
return iv, ciphertext
def getEcc(privatekey=None):
import pyelliptic
global ecc_cache
if privatekey not in ecc_cache:
if privatekey:
publickey_bin = btctools.encode_pubkey(btctools.privtopub(privatekey), "bin")
publickey_openssl = toOpensslPublickey(publickey_bin)
privatekey_openssl = toOpensslPrivatekey(privatekey)
ecc_cache[privatekey] = pyelliptic.ECC(curve='secp256k1', privkey=privatekey_openssl, pubkey=publickey_openssl)
else:
ecc_cache[None] = pyelliptic.ECC()
return ecc_cache[privatekey]
def toOpensslPrivatekey(privatekey):
privatekey_bin = btctools.encode_privkey(privatekey, "bin")
return b'\x02\xca\x00\x20' + privatekey_bin
def toOpensslPublickey(publickey):
publickey_bin = btctools.encode_pubkey(publickey, "bin")
publickey_bin = publickey_bin[1:]
publickey_openssl = b'\x02\xca\x00 ' + publickey_bin[:32] + b'\x00 ' + publickey_bin[32:]
return publickey_openssl

View file

@ -1,26 +1,26 @@
import base64
import os
import gevent
from Plugin import PluginManager
from Crypt import CryptBitcoin, CryptHash
import lib.pybitcointools as btctools
from Config import config
import sslcrypto
from . import CryptMessage
curve = sslcrypto.ecc.get_curve("secp256k1")
@PluginManager.registerTo("UiWebsocket")
class UiWebsocketPlugin(object):
def eciesDecrypt(self, encrypted, privatekey):
back = CryptMessage.getEcc(privatekey).decrypt(encrypted)
return back.decode("utf8")
# - Actions -
# Returns user's public key unique to site
# Return: Public key
def actionUserPublickey(self, to, index=0):
publickey = self.user.getEncryptPublickey(self.site.address, index)
self.response(to, publickey)
self.response(to, self.user.getEncryptPublickey(self.site.address, index))
# Encrypt a text using the publickey or user's sites unique publickey
# Return: Encrypted text using base64 encoding
@ -44,13 +44,7 @@ class UiWebsocketPlugin(object):
else:
encrypted_texts = [param]
texts = [] # Decoded texts
for encrypted_text in encrypted_texts:
try:
text = CryptMessage.eciesDecrypt(encrypted_text, privatekey).decode("utf8")
texts.append(text)
except Exception as err:
texts.append(None)
texts = CryptMessage.eciesDecryptMulti(encrypted_texts, privatekey)
if type(param) == list:
self.response(to, texts)
@ -59,23 +53,16 @@ class UiWebsocketPlugin(object):
# Encrypt a text using AES
# Return: Iv, AES key, Encrypted text
def actionAesEncrypt(self, to, text, key=None, iv=None):
import pyelliptic
def actionAesEncrypt(self, to, text, key=None):
if key:
key = base64.b64decode(key)
else:
key = os.urandom(32)
if iv: # Generate new AES key if not definied
iv = base64.b64decode(iv)
else:
iv = pyelliptic.Cipher.gen_IV('aes-256-cbc')
key = sslcrypto.aes.new_key()
if text:
encrypted = pyelliptic.Cipher(key, iv, 1, ciphername='aes-256-cbc').ciphering(text.encode("utf8"))
encrypted, iv = sslcrypto.aes.encrypt(text.encode("utf8"), key)
else:
encrypted = b""
encrypted, iv = b"", b""
res = [base64.b64encode(item).decode("utf8") for item in [key, iv, encrypted]]
self.response(to, res)
@ -83,8 +70,6 @@ class UiWebsocketPlugin(object):
# Decrypt a text using AES
# Return: Decrypted text
def actionAesDecrypt(self, to, *args):
import pyelliptic
if len(args) == 3: # Single decrypt
encrypted_texts = [(args[0], args[1])]
keys = [args[2]]
@ -97,9 +82,8 @@ class UiWebsocketPlugin(object):
iv = base64.b64decode(iv)
text = None
for key in keys:
ctx = pyelliptic.Cipher(base64.b64decode(key), iv, 0, ciphername='aes-256-cbc')
try:
decrypted = ctx.ciphering(encrypted_text)
decrypted = sslcrypto.aes.decrypt(encrypted_text, iv, base64.b64decode(key))
if decrypted and decrypted.decode("utf8"): # Valid text decoded
text = decrypted.decode("utf8")
except Exception as err:
@ -126,12 +110,11 @@ class UiWebsocketPlugin(object):
# Gets the publickey of a given privatekey
def actionEccPrivToPub(self, to, privatekey):
self.response(to, btctools.privtopub(privatekey))
self.response(to, curve.private_to_public(curve.wif_to_private(privatekey)))
# Gets the address of a given publickey
def actionEccPubToAddr(self, to, publickey):
address = btctools.pubtoaddr(btctools.decode_pubkey(publickey))
self.response(to, address)
self.response(to, curve.public_to_address(bytes.fromhex(publickey)))
@PluginManager.registerTo("User")
@ -167,6 +150,76 @@ class UserPlugin(object):
if "encrypt_publickey_%s" % index not in site_data:
privatekey = self.getEncryptPrivatekey(address, param_index)
publickey = btctools.encode_pubkey(btctools.privtopub(privatekey), "bin_compressed")
publickey = curve.private_to_public(curve.wif_to_private(privatekey))
site_data["encrypt_publickey_%s" % index] = base64.b64encode(publickey).decode("utf8")
return site_data["encrypt_publickey_%s" % index]
@PluginManager.registerTo("Actions")
class ActionsPlugin:
publickey = "A3HatibU4S6eZfIQhVs2u7GLN5G9wXa9WwlkyYIfwYaj"
privatekey = "5JBiKFYBm94EUdbxtnuLi6cvNcPzcKymCUHBDf2B6aq19vvG3rL"
utf8_text = '\xc1rv\xedzt\xfbr\xf5t\xfck\xf6rf\xfar\xf3g\xe9p'
def getBenchmarkTests(self, online=False):
if hasattr(super(), "getBenchmarkTests"):
tests = super().getBenchmarkTests(online)
else:
tests = []
aes_key, encrypted = CryptMessage.eciesEncrypt(self.utf8_text.encode("utf8"), self.publickey) # Warm-up
tests.extend([
{"func": self.testCryptEciesEncrypt, "kwargs": {}, "num": 100, "time_standard": 1.2},
{"func": self.testCryptEciesDecrypt, "kwargs": {}, "num": 500, "time_standard": 1.3},
{"func": self.testCryptEciesDecryptMulti, "kwargs": {}, "num": 5, "time_standard": 0.68},
{"func": self.testCryptAesEncrypt, "kwargs": {}, "num": 10000, "time_standard": 0.27},
{"func": self.testCryptAesDecrypt, "kwargs": {}, "num": 10000, "time_standard": 0.25}
])
return tests
def testCryptEciesEncrypt(self, num_run=1):
for i in range(num_run):
aes_key, encrypted = CryptMessage.eciesEncrypt(self.utf8_text.encode("utf8"), self.publickey)
assert len(aes_key) == 32
yield "."
def testCryptEciesDecrypt(self, num_run=1):
aes_key, encrypted = CryptMessage.eciesEncrypt(self.utf8_text.encode("utf8"), self.publickey)
for i in range(num_run):
assert len(aes_key) == 32
decrypted = CryptMessage.eciesDecrypt(base64.b64encode(encrypted), self.privatekey)
assert decrypted == self.utf8_text.encode("utf8"), "%s != %s" % (decrypted, self.utf8_text.encode("utf8"))
yield "."
def testCryptEciesDecryptMulti(self, num_run=1):
yield "x 100 (%s threads) " % config.threads_crypt
aes_key, encrypted = CryptMessage.eciesEncrypt(self.utf8_text.encode("utf8"), self.publickey)
threads = []
for i in range(num_run):
assert len(aes_key) == 32
threads.append(gevent.spawn(
CryptMessage.eciesDecryptMulti, [base64.b64encode(encrypted)] * 100, self.privatekey
))
for thread in threads:
res = thread.get()
assert res[0] == self.utf8_text, "%s != %s" % (res[0], self.utf8_text)
assert res[0] == res[-1], "%s != %s" % (res[0], res[-1])
yield "."
gevent.joinall(threads)
def testCryptAesEncrypt(self, num_run=1):
for i in range(num_run):
key = os.urandom(32)
encrypted = sslcrypto.aes.encrypt(self.utf8_text.encode("utf8"), key)
yield "."
def testCryptAesDecrypt(self, num_run=1):
key = os.urandom(32)
encrypted_text, iv = sslcrypto.aes.encrypt(self.utf8_text.encode("utf8"), key)
for i in range(num_run):
decrypted = sslcrypto.aes.decrypt(encrypted_text, iv, key).decode("utf8")
assert decrypted == self.utf8_text
yield "."

View file

@ -18,13 +18,10 @@ class TestCrypt:
assert len(aes_key) == 32
# assert len(encrypted) == 134 + int(len(text) / 16) * 16 # Not always true
ecc = CryptMessage.getEcc(self.privatekey)
assert ecc.decrypt(encrypted) == text_repeated
assert CryptMessage.eciesDecrypt(base64.b64encode(encrypted), self.privatekey) == text_repeated
def testDecryptEcies(self, user):
encrypted = base64.b64decode(self.ecies_encrypted_text)
ecc = CryptMessage.getEcc(self.privatekey)
assert ecc.decrypt(encrypted) == b"hello"
assert CryptMessage.eciesDecrypt(self.ecies_encrypted_text, self.privatekey) == b"hello"
def testPublickey(self, ui_websocket):
pub = ui_websocket.testAction("UserPublickey", 0)
@ -47,26 +44,25 @@ class TestCrypt:
assert pub1 != pub2
def testEcies(self, ui_websocket):
ui_websocket.actionUserPublickey(0, 0)
pub = ui_websocket.ws.getResult()
pub = ui_websocket.testAction("UserPublickey")
ui_websocket.actionEciesEncrypt(0, "hello", pub)
encrypted = ui_websocket.ws.getResult()
encrypted = ui_websocket.testAction("EciesEncrypt", "hello", pub)
assert len(encrypted) == 180
# Don't allow decrypt using other privatekey index
ui_websocket.actionEciesDecrypt(0, encrypted, 123)
decrypted = ui_websocket.ws.getResult()
decrypted = ui_websocket.testAction("EciesDecrypt", encrypted, 123)
assert decrypted != "hello"
# Decrypt using correct privatekey
ui_websocket.actionEciesDecrypt(0, encrypted)
decrypted = ui_websocket.ws.getResult()
decrypted = ui_websocket.testAction("EciesDecrypt", encrypted)
assert decrypted == "hello"
# Decrypt incorrect text
decrypted = ui_websocket.testAction("EciesDecrypt", "baad")
assert decrypted is None
# Decrypt batch
ui_websocket.actionEciesDecrypt(0, [encrypted, "baad", encrypted])
decrypted = ui_websocket.ws.getResult()
decrypted = ui_websocket.testAction("EciesDecrypt", [encrypted, "baad", encrypted])
assert decrypted == ["hello", None, "hello"]
def testEciesUtf8(self, ui_websocket):
@ -91,6 +87,21 @@ class TestCrypt:
ui_websocket.actionAesDecrypt(0, base64.b64encode(aes_iv), base64.b64encode(aes_encrypted), aes_key)
assert ui_websocket.ws.getResult() == "hello"
def testEciesAesLongpubkey(self, ui_websocket):
privatekey = "5HwVS1bTFnveNk9EeGaRenWS1QFzLFb5kuncNbiY3RiHZrVR6ok"
ecies_encrypted, aes_key = ["lWiXfEikIjw1ac3J/RaY/gLKACALRUfksc9rXYRFyKDSaxhwcSFBYCgAdIyYlY294g/6VgAf/68PYBVMD3xKH1n7Zbo+ge8b4i/XTKmCZRJvy0eutMKWckYCMVcxgIYNa/ZL1BY1kvvH7omgzg1wBraoLfdbNmVtQgdAZ9XS8PwRy6OB2Q==", "Rvlf7zsMuBFHZIGHcbT1rb4If+YTmsWDv6kGwcvSeMM="]
# Decrypt using Ecies
ui_websocket.actionEciesDecrypt(0, ecies_encrypted, privatekey)
assert ui_websocket.ws.getResult() == "hello"
# Decrypt using AES
aes_iv, aes_encrypted = CryptMessage.split(base64.b64decode(ecies_encrypted))
ui_websocket.actionAesDecrypt(0, base64.b64encode(aes_iv), base64.b64encode(aes_encrypted), aes_key)
assert ui_websocket.ws.getResult() == "hello"
def testAes(self, ui_websocket):
ui_websocket.actionAesEncrypt(0, "hello")
key, iv, encrypted = ui_websocket.ws.getResult()

View file

@ -0,0 +1,5 @@
{
"name": "CryptMessage",
"description": "Cryptographic functions of ECIES and AES data encryption/decryption.",
"default": "enabled"
}

View file

@ -21,9 +21,6 @@ def openArchive(archive_path, file_obj=None):
if archive_path.endswith("tar.gz"):
import tarfile
archive_cache[archive_path] = tarfile.open(archive_path, fileobj=file_obj, mode="r:gz")
elif archive_path.endswith("tar.bz2"):
import tarfile
archive_cache[archive_path] = tarfile.open(archive_path, fileobj=file_obj, mode="r:bz2")
else:
import zipfile
archive_cache[archive_path] = zipfile.ZipFile(file_obj or archive_path)
@ -48,7 +45,7 @@ class UiRequestPlugin(object):
file_obj = None
path_parts = self.parsePath(path)
file_path = "%s/%s/%s" % (config.data_dir, path_parts["address"], path_parts["inner_path"])
match = re.match("^(.*\.(?:tar.gz|tar.bz2|zip))/(.*)", file_path)
match = re.match("^(.*\.(?:tar.gz|zip))/(.*)", file_path)
archive_path, path_within = match.groups()
if archive_path not in archive_cache:
site = self.server.site_manager.get(path_parts["address"])
@ -102,7 +99,7 @@ class UiRequestPlugin(object):
class SiteStoragePlugin(object):
def isFile(self, inner_path):
if ".zip/" in inner_path or ".tar.gz/" in inner_path:
match = re.match("^(.*\.(?:tar.gz|tar.bz2|zip))/(.*)", inner_path)
match = re.match("^(.*\.(?:tar.gz|zip))/(.*)", inner_path)
archive_inner_path, path_within = match.groups()
return super(SiteStoragePlugin, self).isFile(archive_inner_path)
else:
@ -130,7 +127,7 @@ class SiteStoragePlugin(object):
def walk(self, inner_path, *args, **kwags):
if ".zip" in inner_path or ".tar.gz" in inner_path:
match = re.match("^(.*\.(?:tar.gz|tar.bz2|zip))(.*)", inner_path)
match = re.match("^(.*\.(?:tar.gz|zip))(.*)", inner_path)
archive_inner_path, path_within = match.groups()
archive = self.openArchive(archive_inner_path)
path_within = path_within.lstrip("/")
@ -154,7 +151,7 @@ class SiteStoragePlugin(object):
def list(self, inner_path, *args, **kwags):
if ".zip" in inner_path or ".tar.gz" in inner_path:
match = re.match("^(.*\.(?:tar.gz|tar.bz2|zip))(.*)", inner_path)
match = re.match("^(.*\.(?:tar.gz|zip))(.*)", inner_path)
archive_inner_path, path_within = match.groups()
archive = self.openArchive(archive_inner_path)
path_within = path_within.lstrip("/")
@ -179,9 +176,9 @@ class SiteStoragePlugin(object):
else:
return super(SiteStoragePlugin, self).list(inner_path, *args, **kwags)
def read(self, inner_path, mode="rb"):
def read(self, inner_path, mode="rb", **kwargs):
if ".zip/" in inner_path or ".tar.gz/" in inner_path:
match = re.match("^(.*\.(?:tar.gz|tar.bz2|zip))(.*)", inner_path)
match = re.match("^(.*\.(?:tar.gz|zip))(.*)", inner_path)
archive_inner_path, path_within = match.groups()
archive = self.openArchive(archive_inner_path)
path_within = path_within.lstrip("/")
@ -192,5 +189,5 @@ class SiteStoragePlugin(object):
return archive.extractfile(path_within).read()
else:
return super(SiteStoragePlugin, self).read(inner_path, mode)
return super(SiteStoragePlugin, self).read(inner_path, mode, **kwargs)

View file

@ -0,0 +1,5 @@
{
"name": "FilePack",
"description": "Transparent web access for Zip and Tar.gz files.",
"default": "enabled"
}

View file

@ -1,11 +1,13 @@
import re
import time
import copy
import os
from Plugin import PluginManager
from Translate import Translate
from util import RateLimit
from util import helper
from util.Flag import flag
from Debug import Debug
try:
import OptionalManager.UiWebsocketPlugin # To make optioanlFileInfo merger sites compatible
@ -18,8 +20,11 @@ if "merger_db" not in locals().keys(): # To keep merger_sites between module re
merged_to_merger = {} # {address: [site1, site2, ...]} cache
site_manager = None # Site manager for merger sites
plugin_dir = os.path.dirname(__file__)
if "_" not in locals():
_ = Translate("plugins/MergerSite/languages/")
_ = Translate(plugin_dir + "/languages/")
# Check if the site has permission to this merger site
@ -74,14 +79,18 @@ class UiWebsocketPlugin(object):
def cbMergerSiteAdd(self, to, addresses):
added = 0
for address in addresses:
added += 1
try:
site_manager.need(address)
added += 1
except Exception as err:
self.cmd("notification", ["error", _["Adding <b>%s</b> failed: %s"] % (address, err)])
if added:
self.cmd("notification", ["done", _["Added <b>%s</b> new site"] % added, 5000])
RateLimit.called(self.site.address + "-MergerSiteAdd")
site_manager.updateMergerSites()
# Delete a merged site
@flag.no_multiuser
def actionMergerSiteDelete(self, to, address):
site = self.server.sites.get(address)
if not site:
@ -221,7 +230,7 @@ class UiWebsocketPlugin(object):
site = self.server.sites.get(address)
try:
merged_sites.append(site.content_manager.contents.get("content.json").get("title", address))
except Exception as err:
except Exception:
merged_sites.append(address)
details = _["Read and write permissions to sites with merged type of <b>%s</b> "] % merger_type
@ -289,6 +298,9 @@ class SiteStoragePlugin(object):
# Also notice merger sites on a merged site file change
def onUpdated(self, inner_path, file=None):
if inner_path == "content.json":
site_manager.updateMergerSites()
super(SiteStoragePlugin, self).onUpdated(inner_path, file)
merged_type = merged_db.get(self.site.address)
@ -333,9 +345,9 @@ class SiteManagerPlugin(object):
def updateMergerSites(self):
global merger_db, merged_db, merged_to_merger, site_manager
s = time.time()
merger_db = {}
merged_db = {}
merged_to_merger = {}
merger_db_new = {}
merged_db_new = {}
merged_to_merger_new = {}
site_manager = self
if not self.sites:
return
@ -347,7 +359,7 @@ class SiteManagerPlugin(object):
self.log.error("Error loading site %s: %s" % (site.address, Debug.formatException(err)))
continue
if merged_type:
merged_db[site.address] = merged_type
merged_db_new[site.address] = merged_type
# Update merger sites
for permission in site.settings["permissions"]:
@ -361,18 +373,24 @@ class SiteManagerPlugin(object):
site.settings["permissions"].remove(permission)
continue
merger_type = permission.replace("Merger:", "")
if site.address not in merger_db:
merger_db[site.address] = []
merger_db[site.address].append(merger_type)
if site.address not in merger_db_new:
merger_db_new[site.address] = []
merger_db_new[site.address].append(merger_type)
site_manager.sites[site.address] = site
# Update merged to merger
if merged_type:
for merger_site in self.sites.values():
if "Merger:" + merged_type in merger_site.settings["permissions"]:
if site.address not in merged_to_merger:
merged_to_merger[site.address] = []
merged_to_merger[site.address].append(merger_site)
if site.address not in merged_to_merger_new:
merged_to_merger_new[site.address] = []
merged_to_merger_new[site.address].append(merger_site)
# Update globals
merger_db = merger_db_new
merged_db = merged_db_new
merged_to_merger = merged_to_merger_new
self.log.debug("Updated merger sites in %.3fs" % (time.time() - s))
def load(self, *args, **kwags):

View file

@ -5,6 +5,7 @@ from Plugin import PluginManager
from Db.DbQuery import DbQuery
from Debug import Debug
from util import helper
from util.Flag import flag
@PluginManager.registerTo("UiWebsocket")
@ -27,10 +28,8 @@ class UiWebsocketPlugin(object):
feeds = self.user.sites.get(self.site.address, {}).get("follow", {})
self.response(to, feeds)
@flag.admin
def actionFeedQuery(self, to, limit=10, day_limit=3):
if "ADMIN" not in self.site.settings["permissions"]:
return self.response(to, "FeedQuery not allowed")
from Site import SiteManager
rows = []
stats = []

View file

@ -24,8 +24,8 @@ class ContentDbPlugin(object):
self.time_peer_numbers_updated = 0
self.my_optional_files = {} # Last 50 site_address/inner_path called by fileWrite (auto-pinning these files)
self.optional_files = collections.defaultdict(dict)
self.optional_files_loading = False
helper.timer(60 * 5, self.checkOptionalLimit)
self.optional_files_loaded = False
self.timer_check_optional = helper.timer(60 * 5, self.checkOptionalLimit)
super(ContentDbPlugin, self).__init__(*args, **kwargs)
def getSchema(self):
@ -60,9 +60,6 @@ class ContentDbPlugin(object):
super(ContentDbPlugin, self).initSite(site)
if self.need_filling:
self.fillTableFileOptional(site)
if not self.optional_files_loading:
gevent.spawn_later(1, self.loadFilesOptional)
self.optional_files_loading = True
def checkTables(self):
changed_tables = super(ContentDbPlugin, self).checkTables()
@ -91,7 +88,7 @@ class ContentDbPlugin(object):
site_ids_reverse = {val: key for key, val in self.site_ids.items()}
for site_id, stats in site_sizes.items():
site_address = site_ids_reverse.get(site_id)
if not site_address:
if not site_address or site_address not in self.sites:
self.log.error("Not found site_id: %s" % site_id)
continue
site = self.sites[site_address]
@ -100,7 +97,7 @@ class ContentDbPlugin(object):
total += stats["size_optional"]
total_downloaded += stats["optional_downloaded"]
self.log.debug(
self.log.info(
"Loaded %s optional files: %.2fMB, downloaded: %.2fMB in %.3fs" %
(num, float(total) / 1024 / 1024, float(total_downloaded) / 1024 / 1024, time.time() - s)
)
@ -108,7 +105,7 @@ class ContentDbPlugin(object):
if self.need_filling and self.getOptionalLimitBytes() >= 0 and self.getOptionalLimitBytes() < total_downloaded:
limit_bytes = self.getOptionalLimitBytes()
limit_new = round((float(total_downloaded) / 1024 / 1024 / 1024) * 1.1, 2) # Current limit + 10%
self.log.debug(
self.log.info(
"First startup after update and limit is smaller than downloaded files size (%.2fGB), increasing it from %.2fGB to %.2fGB" %
(float(total_downloaded) / 1024 / 1024 / 1024, float(limit_bytes) / 1024 / 1024 / 1024, limit_new)
)
@ -142,14 +139,14 @@ class ContentDbPlugin(object):
if not user:
user = UserManager.user_manager.create()
auth_address = user.getAuthAddress(site.address)
self.execute(
res = self.execute(
"UPDATE file_optional SET is_pinned = 1 WHERE site_id = :site_id AND inner_path LIKE :inner_path",
{"site_id": site_id, "inner_path": "%%/%s/%%" % auth_address}
)
self.log.debug(
"Filled file_optional table for %s in %.3fs (loaded: %s, is_pinned: %s)" %
(site.address, time.time() - s, num, self.cur.cursor.rowcount)
(site.address, time.time() - s, num, res.rowcount)
)
self.filled[site.address] = True
@ -405,3 +402,13 @@ class ContentDbPlugin(object):
for file_id in deleted_file_ids:
cur.execute("UPDATE file_optional SET is_downloaded = 0, is_pinned = 0, peer = peer - 1 WHERE ?", {"file_id": file_id})
cur.close()
@PluginManager.registerTo("SiteManager")
class SiteManagerPlugin(object):
def load(self, *args, **kwargs):
back = super(SiteManagerPlugin, self).load(*args, **kwargs)
if self.sites and not content_db.optional_files_loaded and content_db.conn:
content_db.optional_files_loaded = True
content_db.loadFilesOptional()
return back

View file

@ -17,32 +17,46 @@ def importPluginnedClasses():
def processAccessLog():
global access_log
if access_log:
content_db = ContentDbPlugin.content_db
if not content_db.conn:
return False
s = time.time()
access_log_prev = access_log
access_log = collections.defaultdict(dict)
now = int(time.time())
num = 0
for site_id in access_log:
for site_id in access_log_prev:
content_db.execute(
"UPDATE file_optional SET time_accessed = %s WHERE ?" % now,
{"site_id": site_id, "inner_path": list(access_log[site_id].keys())}
{"site_id": site_id, "inner_path": list(access_log_prev[site_id].keys())}
)
num += len(access_log[site_id])
access_log.clear()
num += len(access_log_prev[site_id])
content_db.log.debug("Inserted %s web request stat in %.3fs" % (num, time.time() - s))
def processRequestLog():
global request_log
if request_log:
content_db = ContentDbPlugin.content_db
cur = content_db.getCursor()
if not content_db.conn:
return False
s = time.time()
request_log_prev = request_log
request_log = collections.defaultdict(lambda: collections.defaultdict(int)) # {site_id: {inner_path1: 1, inner_path2: 1...}}
num = 0
for site_id in request_log:
for inner_path, uploaded in request_log[site_id].items():
for site_id in request_log_prev:
for inner_path, uploaded in request_log_prev[site_id].items():
content_db.execute(
"UPDATE file_optional SET uploaded = uploaded + %s WHERE ?" % uploaded,
{"site_id": site_id, "inner_path": inner_path}
)
num += 1
request_log.clear()
content_db.log.debug("Inserted %s file request stat in %.3fs" % (num, time.time() - s))
if "access_log" not in locals().keys(): # To keep between module reloads
@ -72,12 +86,12 @@ class ContentManagerPlugin(object):
return super(ContentManagerPlugin, self).optionalDownloaded(inner_path, hash_id, size, own)
def optionalRemoved(self, inner_path, hash_id, size=None):
self.contents.db.execute(
res = self.contents.db.execute(
"UPDATE file_optional SET is_downloaded = 0, is_pinned = 0, peer = peer - 1 WHERE site_id = :site_id AND inner_path = :inner_path AND is_downloaded = 1",
{"site_id": self.contents.db.site_ids[self.site.address], "inner_path": inner_path}
)
if self.contents.db.cur.cursor.rowcount > 0:
if res.rowcount > 0:
back = super(ContentManagerPlugin, self).optionalRemoved(inner_path, hash_id, size)
# Re-add to hashfield if we have other file with the same hash_id
if self.isDownloaded(hash_id=hash_id, force_check_db=True):

View file

@ -1,16 +1,21 @@
import re
import time
import html
import os
import gevent
from Plugin import PluginManager
from Config import config
from util import helper
from util.Flag import flag
from Translate import Translate
plugin_dir = os.path.dirname(__file__)
if "_" not in locals():
_ = Translate("plugins/OptionalManager/languages/")
_ = Translate(plugin_dir + "/languages/")
bigfile_sha512_cache = {}
@ -210,6 +215,7 @@ class UiWebsocketPlugin(object):
return "ok"
@flag.no_multiuser
def actionOptionalFilePin(self, to, inner_path, address=None):
if type(inner_path) is not list:
inner_path = [inner_path]
@ -222,6 +228,7 @@ class UiWebsocketPlugin(object):
self.cmd("notification", ["done", _["Pinned %s files"] % num_file, 5000])
self.response(to, back)
@flag.no_multiuser
def actionOptionalFileUnpin(self, to, inner_path, address=None):
if type(inner_path) is not list:
inner_path = [inner_path]
@ -234,6 +241,7 @@ class UiWebsocketPlugin(object):
self.cmd("notification", ["done", _["Removed pin from %s files"] % num_file, 5000])
self.response(to, back)
@flag.no_multiuser
def actionOptionalFileDelete(self, to, inner_path, address=None):
if not address:
address = self.site.address
@ -271,10 +279,8 @@ class UiWebsocketPlugin(object):
# Limit functions
@flag.admin
def actionOptionalLimitStats(self, to):
if "ADMIN" not in self.site.settings["permissions"]:
return self.response(to, "Forbidden")
back = {}
back["limit"] = config.optional_limit
back["used"] = self.site.content_manager.contents.db.getOptionalUsedBytes()
@ -282,10 +288,10 @@ class UiWebsocketPlugin(object):
self.response(to, back)
@flag.no_multiuser
@flag.admin
def actionOptionalLimitSet(self, to, limit):
if "ADMIN" not in self.site.settings["permissions"]:
return self.response(to, {"error": "Forbidden"})
config.optional_limit = re.sub("\.0+$", "", limit) # Remove unnecessary digits from end
config.optional_limit = re.sub(r"\.0+$", "", limit) # Remove unnecessary digits from end
config.saveValue("optional_limit", limit)
self.response(to, "ok")
@ -302,6 +308,7 @@ class UiWebsocketPlugin(object):
self.response(to, site.settings.get("optional_help", {}))
@flag.no_multiuser
def actionOptionalHelp(self, to, directory, title, address=None):
if not address:
address = self.site.address
@ -338,6 +345,7 @@ class UiWebsocketPlugin(object):
self.response(to, dict(stats))
@flag.no_multiuser
def actionOptionalHelpRemove(self, to, directory, address=None):
if not address:
address = self.site.address
@ -357,6 +365,7 @@ class UiWebsocketPlugin(object):
site.settings["autodownloadoptional"] = value
self.response(to, value)
@flag.no_multiuser
def actionOptionalHelpAll(self, to, value, address=None):
if not address:
address = self.site.address

View file

@ -1 +1,2 @@
from . import OptionalManagerPlugin
from . import UiWebsocketPlugin

View file

@ -70,7 +70,7 @@ class ContentDbPlugin(object):
def savePeers(self, site, spawn=False):
if spawn:
# Save peers every hour (+random some secs to not update very site at same time)
gevent.spawn_later(60 * 60 + random.randint(0, 60), self.savePeers, site, spawn=True)
site.greenlet_manager.spawnLater(60 * 60 + random.randint(0, 60), self.savePeers, site, spawn=True)
if not site.peers:
site.log.debug("Peers not saved: No peers found")
return
@ -79,7 +79,7 @@ class ContentDbPlugin(object):
cur = self.getCursor()
try:
cur.execute("DELETE FROM peer WHERE site_id = :site_id", {"site_id": site_id})
cur.cursor.executemany(
cur.executemany(
"INSERT INTO peer (site_id, address, port, hashfield, reputation, time_added, time_found) VALUES (?, ?, ?, ?, ?, ?, ?)",
self.iteratePeers(site)
)
@ -89,8 +89,8 @@ class ContentDbPlugin(object):
def initSite(self, site):
super(ContentDbPlugin, self).initSite(site)
gevent.spawn_later(0.5, self.loadPeers, site)
gevent.spawn_later(60*60, self.savePeers, site, spawn=True)
site.greenlet_manager.spawnLater(0.5, self.loadPeers, site)
site.greenlet_manager.spawnLater(60*60, self.savePeers, site, spawn=True)
def saveAllPeers(self):
for site in list(self.sites.values()):

View file

@ -0,0 +1,5 @@
{
"name": "PeerDb",
"description": "Save/restore peer list on client restart.",
"default": "enabled"
}

View file

@ -5,6 +5,7 @@ from Plugin import PluginManager
from Config import config
from Debug import Debug
from util import SafeRe
from util.Flag import flag
class WsLogStreamer(logging.StreamHandler):
@ -37,10 +38,11 @@ class WsLogStreamer(logging.StreamHandler):
@PluginManager.registerTo("UiWebsocket")
class UiWebsocketPlugin(object):
def __init__(self, *args, **kwargs):
self.admin_commands.update(["consoleLogRead", "consoleLogStream", "consoleLogStreamRemove"])
self.log_streamers = {}
return super(UiWebsocketPlugin, self).__init__(*args, **kwargs)
@flag.no_multiuser
@flag.admin
def actionConsoleLogRead(self, to, filter=None, read_size=32 * 1024, limit=500):
log_file_path = "%s/debug.log" % config.log_dir
log_file = open(log_file_path, encoding="utf-8")
@ -56,9 +58,16 @@ class UiWebsocketPlugin(object):
assert SafeRe.isSafePattern(filter)
filter_re = re.compile(".*" + filter)
last_match = False
for line in log_file:
if filter and not filter_re.match(line):
if not line.startswith("[") and last_match: # Multi-line log entry
lines.append(line.replace(" ", "&nbsp;"))
continue
if filter and not filter_re.match(line):
last_match = False
continue
last_match = True
lines.append(line)
num_found = len(lines)
@ -74,11 +83,15 @@ class UiWebsocketPlugin(object):
logging.getLogger('').addHandler(logger)
return logger
@flag.no_multiuser
@flag.admin
def actionConsoleLogStream(self, to, filter=None):
stream_id = to
self.log_streamers[stream_id] = self.addLogStreamer(stream_id, filter)
self.response(to, {"stream_id": stream_id})
@flag.no_multiuser
@flag.admin
def actionConsoleLogStreamRemove(self, to, stream_id):
try:
self.log_streamers[stream_id].stop()

View file

@ -11,16 +11,17 @@ import urllib.parse
import gevent
import util
from Config import config
from Plugin import PluginManager
from Debug import Debug
from Translate import Translate
from util import helper
from util.Flag import flag
from .ZipStream import ZipStream
plugin_dir = "plugins/Sidebar"
plugin_dir = os.path.dirname(__file__)
media_dir = plugin_dir + "/media"
sys.path.append(plugin_dir) # To able to load geoip lib
loc_cache = {}
if "_" not in locals():
@ -86,10 +87,6 @@ class UiRequestPlugin(object):
@PluginManager.registerTo("UiWebsocket")
class UiWebsocketPlugin(object):
def __init__(self, *args, **kwargs):
self.async_commands.add("sidebarGetPeers")
return super(UiWebsocketPlugin, self).__init__(*args, **kwargs)
def sidebarRenderPeerStats(self, body, site):
connected = len([peer for peer in list(site.peers.values()) if peer.connection and peer.connection.connected])
connectable = len([peer_id for peer_id in list(site.peers.keys()) if not peer_id.endswith(":0")])
@ -318,16 +315,18 @@ class UiWebsocketPlugin(object):
body.append(_("""
<li>
<label>{_[Download and help distribute all files]}</label>
<label>{_[Help distribute added optional files]}</label>
<input type="checkbox" class="checkbox" id="checkbox-autodownloadoptional" {checked}/><div class="checkbox-skin"></div>
"""))
if hasattr(config, "autodownload_bigfile_size_limit"):
autodownload_bigfile_size_limit = int(site.settings.get("autodownload_bigfile_size_limit", config.autodownload_bigfile_size_limit))
body.append(_("""
<div class='settings-autodownloadoptional'>
<label>{_[Auto download big file size limit]}</label>
<input type='text' class='text text-num' value="{autodownload_bigfile_size_limit}" id='input-autodownload_bigfile_size_limit'/><span class='text-post'>MB</span>
<a href='#Set' class='button' id='button-autodownload_bigfile_size_limit'>{_[Set]}</a>
<a href='#Download+previous' class='button' id='button-autodownload_previous'>{_[Download previous files]}</a>
</div>
"""))
body.append("</li>")
@ -485,7 +484,7 @@ class UiWebsocketPlugin(object):
def sidebarRenderContents(self, body, site):
has_privatekey = bool(self.user.getSiteData(site.address, create=False).get("privatekey"))
if has_privatekey:
tag_privatekey = _("{_[Private key saved.]} <a href='#Forgot+private+key' id='privatekey-forgot' class='link-right'>{_[Forgot]}</a>")
tag_privatekey = _("{_[Private key saved.]} <a href='#Forget+private+key' id='privatekey-forget' class='link-right'>{_[Forget]}</a>")
else:
tag_privatekey = _("<a href='#Add+private+key' id='privatekey-add' class='link-right'>{_[Add saved private key]}</a>")
@ -511,11 +510,8 @@ class UiWebsocketPlugin(object):
body.append("</div>")
body.append("</li>")
@flag.admin
def actionSidebarGetHtmlTag(self, to):
permissions = self.getPermissions(to)
if "ADMIN" not in permissions:
return self.response(to, "You don't have permission to run this command")
site = self.site
body = []
@ -566,7 +562,7 @@ class UiWebsocketPlugin(object):
self.log.info("Downloading GeoLite2 City database...")
self.cmd("progress", ["geolite-info", _["Downloading GeoLite2 City database (one time only, ~20MB)..."], 0])
db_urls = [
"https://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz",
"https://raw.githubusercontent.com/aemr3/GeoLite2-Database/master/GeoLite2-City.mmdb.gz",
"https://raw.githubusercontent.com/texnikru/GeoLite2-Database/master/GeoLite2-City.mmdb.gz"
]
for db_url in db_urls:
@ -635,6 +631,7 @@ class UiWebsocketPlugin(object):
loc_cache[ip] = loc
return loc
@util.Noparallel()
def getGeoipDb(self):
db_name = 'GeoLite2-City.mmdb'
@ -664,7 +661,6 @@ class UiWebsocketPlugin(object):
self.log.debug("Not showing peer locations: no GeoIP database")
return False
self.log.info("Loading GeoIP database from: %s" % db_path)
geodb = maxminddb.open_database(db_path)
peers = list(peers.values())
@ -706,11 +702,9 @@ class UiWebsocketPlugin(object):
return peer_locations
@flag.admin
@flag.async_run
def actionSidebarGetPeers(self, to):
permissions = self.getPermissions(to)
if "ADMIN" not in permissions:
return self.response(to, "You don't have permission to run this command")
try:
peer_locations = self.getPeerLocations(self.site.peers)
globe_data = []
@ -739,53 +733,41 @@ class UiWebsocketPlugin(object):
self.log.debug("sidebarGetPeers error: %s" % Debug.formatException(err))
self.response(to, {"error": str(err)})
@flag.admin
@flag.no_multiuser
def actionSiteSetOwned(self, to, owned):
permissions = self.getPermissions(to)
if "ADMIN" not in permissions:
return self.response(to, "You don't have permission to run this command")
if self.site.address == config.updatesite:
return self.response(to, "You can't change the ownership of the updater site")
self.site.settings["own"] = bool(owned)
self.site.updateWebsocket(owned=owned)
@flag.admin
@flag.no_multiuser
def actionUserSetSitePrivatekey(self, to, privatekey):
permissions = self.getPermissions(to)
if "ADMIN" not in permissions:
return self.response(to, "You don't have permission to run this command")
site_data = self.user.sites[self.site.address]
site_data["privatekey"] = privatekey
self.site.updateWebsocket(set_privatekey=bool(privatekey))
return "ok"
@flag.admin
@flag.no_multiuser
def actionSiteSetAutodownloadoptional(self, to, owned):
permissions = self.getPermissions(to)
if "ADMIN" not in permissions:
return self.response(to, "You don't have permission to run this command")
self.site.settings["autodownloadoptional"] = bool(owned)
self.site.bad_files = {}
gevent.spawn(self.site.update, check_files=True)
self.site.worker_manager.removeSolvedFileTasks()
@flag.no_multiuser
@flag.admin
def actionDbReload(self, to):
permissions = self.getPermissions(to)
if "ADMIN" not in permissions:
return self.response(to, "You don't have permission to run this command")
self.site.storage.closeDb()
self.site.storage.getDb()
return self.response(to, "ok")
@flag.no_multiuser
@flag.admin
def actionDbRebuild(self, to):
permissions = self.getPermissions(to)
if "ADMIN" not in permissions:
return self.response(to, "You don't have permission to run this command")
try:
self.site.storage.rebuildDb()
except Exception as err:

View file

@ -76,7 +76,7 @@
"Delete this site": "Az oldal törlése",
"File write error: ": "Fájl írási hiba: ",
"Site settings saved!": "Az oldal beállításai elmentve!",
"Enter your private key:": "Add meg a prviát kulcsod:",
"Enter your private key:": "Add meg a privát kulcsod:",
" Signed!": " Aláírva!",
"WebGL not supported": "WebGL nem támogatott"
}

View file

@ -62,7 +62,6 @@
"Save site settings": "Salvar definições do site",
"Open site directory": "Abrir diretório do site",
"Content publishing": "内容发布",
"Content publishing": "Publicação do conteúdo",
"Choose": "Escolher",
"Sign": "Assinar",
@ -93,5 +92,6 @@
"Site settings saved!": "Definições do site salvas!",
"Enter your private key:": "Digite sua chave privada:",
" Signed!": " Assinado!",
"WebGL not supported": "WebGL não é suportado"
"WebGL not supported": "WebGL não é suportado",
"Save as .zip": "Salvar como .zip"
}

View file

@ -27,8 +27,11 @@
"Optional files": "可选文件",
"Downloaded": "已下载",
"Download and help distribute all files": "下载并帮助分发所有文件",
"Help distribute added optional files": "帮助分发新的可选文件",
"Auto download big file size limit": "自动下载大文件大小限制",
"Download previous files": "下载之前的文件",
"Optional files download started": "可选文件下载启动",
"Optional files downloaded": "可选文件下载完成",
"Total size": "总大小",
"Downloaded files": "已下载文件",

View file

@ -3,6 +3,14 @@ class Console extends Class
@tag = null
@opened = false
@filter = null
@tab_types = [
{title: "All", filter: ""},
{title: "Info", filter: "INFO"},
{title: "Warning", filter: "WARNING"},
{title: "Error", filter: "ERROR"}
]
@read_size = 32 * 1024
@tab_active = ""
#@filter = @sidebar.wrapper.site_info.address_short
handleMessageWebsocket_original = @sidebar.wrapper.handleMessageWebsocket
@sidebar.wrapper.handleMessageWebsocket = (message) =>
@ -11,7 +19,11 @@ class Console extends Class
else
handleMessageWebsocket_original(message)
if window.top.location.hash == "#console"
$(window).on "hashchange", =>
if window.top.location.hash == "#ZeroNet:Console"
@open()
if window.top.location.hash == "#ZeroNet:Console"
setTimeout (=> @open()), 10
createHtmltag: ->
@ -20,6 +32,7 @@ class Console extends Class
<div class="console-container">
<div class="console">
<div class="console-top">
<div class="console-tabs"></div>
<div class="console-text">Loading...</div>
</div>
<div class="console-middle">
@ -33,6 +46,7 @@ class Console extends Class
""")
@text = @container.find(".console-text")
@text_elem = @text[0]
@tabs = @container.find(".console-tabs")
@text.on "mousewheel", (e) => # Stop animation on manual scrolling
if e.originalEvent.deltaY < 0
@ -43,6 +57,12 @@ class Console extends Class
@container.appendTo(document.body)
@tag = @container.find(".console")
for tab_type in @tab_types
tab = $("<a></a>", {href: "#", "data-filter": tab_type.filter}).text(tab_type.title)
if tab_type.filter == @tab_active
tab.addClass("active")
tab.on("click", @handleTabClick)
@tabs.append(tab)
@container.on "mousedown touchend touchcancel", (e) =>
if e.target != e.currentTarget
@ -94,26 +114,31 @@ class Console extends Class
loadConsoleText: =>
@sidebar.wrapper.ws.cmd "consoleLogRead", {filter: @filter}, (res) =>
@sidebar.wrapper.ws.cmd "consoleLogRead", {filter: @filter, read_size: @read_size}, (res) =>
@text.html("")
pos_diff = res["pos_end"] - res["pos_start"]
size_read = Math.round(pos_diff/1024)
size_total = Math.round(res['pos_end']/1024)
@text.append("<br><br>")
@text.append("Displaying #{res.lines.length} of #{res.num_found} lines found in the last #{size_read}kB of the log file. (#{size_total}kB total)<br>")
@addLines res.lines, false
@text_elem.scrollTop = @text_elem.scrollHeight
if @stream_id
@sidebar.wrapper.ws.cmd "consoleLogStreamRemove", {stream_id: @stream_id}
@sidebar.wrapper.ws.cmd "consoleLogStream", {filter: @filter}, (res) =>
@stream_id = res.stream_id
close: =>
window.top.location.hash = ""
@sidebar.move_lock = "y"
@sidebar.startDrag()
@sidebar.stopDrag()
open: =>
@createHtmltag()
@sidebar.fixbutton_targety = @sidebar.page_height
@stopDragY()
@sidebar.startDrag()
@sidebar.moved("y")
@sidebar.fixbutton_targety = @sidebar.page_height - @sidebar.fixbutton_inity - 50
@sidebar.stopDrag()
onOpened: =>
@sidebar.onClosed()
@ -153,4 +178,20 @@ class Console extends Class
if not @opened
@onClosed()
changeFilter: (filter) =>
@filter = filter
if @filter == ""
@read_size = 32 * 1024
else
@read_size = 5 * 1024 * 1024
@loadConsoleText()
handleTabClick: (e) =>
elem = $(e.currentTarget)
@tab_active = elem.data("filter")
$("a", @tabs).removeClass("active")
elem.addClass("active")
@changeFilter(@tab_active)
return false
window.Console = Console

View file

@ -1,9 +1,19 @@
.console-container { width: 100%; z-index: 998; position: absolute; top: -100vh; padding-bottom: 100%; }
.console-container .console { background-color: #212121; height: 100vh; transform: translateY(0px); padding-top: 80px; box-sizing: border-box; }
.console-top { color: white; font-family: Consolas, monospace; font-size: 11px; line-height: 20px; padding: 5px; height: 100%; box-sizing: border-box; letter-spacing: 0.5px;}
.console-text { overflow-y: scroll; height: 100%; color: #DDD; }
.console-top { color: white; font-family: Consolas, monospace; font-size: 11px; line-height: 20px; height: 100%; box-sizing: border-box; letter-spacing: 0.5px;}
.console-text { overflow-y: scroll; height: calc(100% - 10px); color: #DDD; padding: 5px; margin-top: -36px; overflow-wrap: break-word; }
.console-tabs {
background-color: #41193fad; position: relative; margin-right: 17px; /*backdrop-filter: blur(2px);*/
box-shadow: -30px 0px 45px #7d2463; background: linear-gradient(-75deg, #591a48ed, #70305e66); border-bottom: 1px solid #792e6473;
}
.console-tabs a {
margin-right: 5px; padding: 5px 15px; text-decoration: none; color: #AAA;
font-size: 11px; font-family: "Consolas"; text-transform: uppercase; border: 1px solid #666;
border-bottom: 0px; display: inline-block; margin: 5px; margin-bottom: 0px; background-color: rgba(0,0,0,0.5);
}
.console-tabs a:hover { color: #FFF }
.console-tabs a.active { background-color: #46223c; color: #FFF }
.console-middle {height: 0px; top: 50%; position: absolute; width: 100%; left: 50%; display: none; }
.console .mynode {

View file

@ -23,9 +23,9 @@ class Sidebar extends Class
@original_set_site_info = @wrapper.setSiteInfo # We going to override this, save the original
# Start in opened state for debugging
if false
if window.top.location.hash == "#ZeroNet:OpenSidebar"
@startDrag()
@moved()
@moved("x")
@fixbutton_targetx = @fixbutton_initx - @width
@stopDrag()
@ -204,15 +204,15 @@ class Sidebar extends Class
return true
}
# Save and forgot privatekey for site signing
# Save and forget privatekey for site signing
@tag.find("#privatekey-add").off("click, touchend").on "click touchend", (e) =>
@wrapper.displayPrompt "Enter your private key:", "password", "Save", "", (privatekey) =>
@wrapper.ws.cmd "userSetSitePrivatekey", [privatekey], (res) =>
@wrapper.notifications.add "privatekey", "done", "Private key saved for site signing", 5000
return false
@tag.find("#privatekey-forgot").off("click, touchend").on "click touchend", (e) =>
@wrapper.displayConfirm "Remove saved private key for this site?", "Forgot", (res) =>
@tag.find("#privatekey-forget").off("click, touchend").on "click touchend", (e) =>
@wrapper.displayConfirm "Remove saved private key for this site?", "Forget", (res) =>
if not res
return false
@wrapper.ws.cmd "userSetSitePrivatekey", [""], (res) =>
@ -372,6 +372,14 @@ class Sidebar extends Class
@updateHtmlTag()
return false
# Site start download optional files
@tag.find("#button-autodownload_previous").off("click touchend").on "click touchend", =>
@wrapper.ws.cmd "siteUpdate", {"address": @wrapper.site_info.address, "check_files": true}, =>
@wrapper.notifications.add "done-download_optional", "done", "Optional files downloaded", 5000
@wrapper.notifications.add "start-download_optional", "info", "Optional files download started", 5000
return false
# Database reload
@tag.find("#button-dbreload").off("click touchend").on "click touchend", =>
@wrapper.ws.cmd "dbReload", [], =>

View file

@ -1,13 +1,23 @@
/* ---- plugins/Sidebar/media/Console.css ---- */
/* ---- Console.css ---- */
.console-container { width: 100%; z-index: 998; position: absolute; top: -100vh; padding-bottom: 100%; }
.console-container .console { background-color: #212121; height: 100vh; -webkit-transform: translateY(0px); -moz-transform: translateY(0px); -o-transform: translateY(0px); -ms-transform: translateY(0px); transform: translateY(0px) ; padding-top: 80px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; }
.console-top { color: white; font-family: Consolas, monospace; font-size: 11px; line-height: 20px; padding: 5px; height: 100%; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; letter-spacing: 0.5px;}
.console-text { overflow-y: scroll; height: 100%; color: #DDD; }
.console-top { color: white; font-family: Consolas, monospace; font-size: 11px; line-height: 20px; height: 100%; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; letter-spacing: 0.5px;}
.console-text { overflow-y: scroll; height: calc(100% - 10px); color: #DDD; padding: 5px; margin-top: -36px; overflow-wrap: break-word; }
.console-tabs {
background-color: #41193fad; position: relative; margin-right: 17px; /*backdrop-filter: blur(2px);*/
-webkit-box-shadow: -30px 0px 45px #7d2463; -moz-box-shadow: -30px 0px 45px #7d2463; -o-box-shadow: -30px 0px 45px #7d2463; -ms-box-shadow: -30px 0px 45px #7d2463; box-shadow: -30px 0px 45px #7d2463 ; background: -webkit-linear-gradient(-75deg, #591a48ed, #70305e66);background: -moz-linear-gradient(-75deg, #591a48ed, #70305e66);background: -o-linear-gradient(-75deg, #591a48ed, #70305e66);background: -ms-linear-gradient(-75deg, #591a48ed, #70305e66);background: linear-gradient(-75deg, #591a48ed, #70305e66); border-bottom: 1px solid #792e6473;
}
.console-tabs a {
margin-right: 5px; padding: 5px 15px; text-decoration: none; color: #AAA;
font-size: 11px; font-family: "Consolas"; text-transform: uppercase; border: 1px solid #666;
border-bottom: 0px; display: inline-block; margin: 5px; margin-bottom: 0px; background-color: rgba(0,0,0,0.5);
}
.console-tabs a:hover { color: #FFF }
.console-tabs a.active { background-color: #46223c; color: #FFF }
.console-middle {height: 0px; top: 50%; position: absolute; width: 100%; left: 50%; display: none; }
.console .mynode {
@ -25,7 +35,7 @@
}
/* ---- plugins/Sidebar/media/Menu.css ---- */
/* ---- Menu.css ---- */
.menu {
@ -48,7 +58,7 @@
.menu, .menu.visible { position: absolute; left: unset !important; right: 20px; }
}
/* ---- plugins/Sidebar/media/Scrollbable.css ---- */
/* ---- Scrollbable.css ---- */
.scrollable {
@ -97,7 +107,7 @@
}
/* ---- plugins/Sidebar/media/Sidebar.css ---- */
/* ---- Sidebar.css ---- */
.menu {

View file

@ -1,5 +1,5 @@
/* ---- plugins/Sidebar/media/Class.coffee ---- */
/* ---- Class.coffee ---- */
(function() {
@ -56,7 +56,7 @@
}).call(this);
/* ---- plugins/Sidebar/media/Console.coffee ---- */
/* ---- Console.coffee ---- */
(function() {
@ -71,6 +71,8 @@
function Console(sidebar) {
var handleMessageWebsocket_original;
this.sidebar = sidebar;
this.handleTabClick = bind(this.handleTabClick, this);
this.changeFilter = bind(this.changeFilter, this);
this.stopDragY = bind(this.stopDragY, this);
this.cleanup = bind(this.cleanup, this);
this.onClosed = bind(this.onClosed, this);
@ -84,6 +86,23 @@
this.tag = null;
this.opened = false;
this.filter = null;
this.tab_types = [
{
title: "All",
filter: ""
}, {
title: "Info",
filter: "INFO"
}, {
title: "Warning",
filter: "WARNING"
}, {
title: "Error",
filter: "ERROR"
}
];
this.read_size = 32 * 1024;
this.tab_active = "";
handleMessageWebsocket_original = this.sidebar.wrapper.handleMessageWebsocket;
this.sidebar.wrapper.handleMessageWebsocket = (function(_this) {
return function(message) {
@ -94,7 +113,14 @@
}
};
})(this);
if (window.top.location.hash === "#console") {
$(window).on("hashchange", (function(_this) {
return function() {
if (window.top.location.hash === "#ZeroNet:Console") {
return _this.open();
}
};
})(this));
if (window.top.location.hash === "#ZeroNet:Console") {
setTimeout(((function(_this) {
return function() {
return _this.open();
@ -104,10 +130,12 @@
}
Console.prototype.createHtmltag = function() {
var j, len, ref, tab, tab_type;
if (!this.container) {
this.container = $("<div class=\"console-container\">\n <div class=\"console\">\n <div class=\"console-top\">\n <div class=\"console-text\">Loading...</div>\n </div>\n <div class=\"console-middle\">\n <div class=\"mynode\"></div>\n <div class=\"peers\">\n <div class=\"peer\"><div class=\"line\"></div><a href=\"#\" class=\"icon\">\u25BD</div></div>\n </div>\n </div>\n </div>\n</div>");
this.container = $("<div class=\"console-container\">\n <div class=\"console\">\n <div class=\"console-top\">\n <div class=\"console-tabs\"></div>\n <div class=\"console-text\">Loading...</div>\n </div>\n <div class=\"console-middle\">\n <div class=\"mynode\"></div>\n <div class=\"peers\">\n <div class=\"peer\"><div class=\"line\"></div><a href=\"#\" class=\"icon\">\u25BD</div></div>\n </div>\n </div>\n </div>\n</div>");
this.text = this.container.find(".console-text");
this.text_elem = this.text[0];
this.tabs = this.container.find(".console-tabs");
this.text.on("mousewheel", (function(_this) {
return function(e) {
if (e.originalEvent.deltaY < 0) {
@ -119,6 +147,19 @@
this.text.is_bottom = true;
this.container.appendTo(document.body);
this.tag = this.container.find(".console");
ref = this.tab_types;
for (j = 0, len = ref.length; j < len; j++) {
tab_type = ref[j];
tab = $("<a></a>", {
href: "#",
"data-filter": tab_type.filter
}).text(tab_type.title);
if (tab_type.filter === this.tab_active) {
tab.addClass("active");
}
tab.on("click", this.handleTabClick);
this.tabs.append(tab);
}
this.container.on("mousedown touchend touchcancel", (function(_this) {
return function(e) {
if (e.target !== e.currentTarget) {
@ -194,7 +235,8 @@
Console.prototype.loadConsoleText = function() {
this.sidebar.wrapper.ws.cmd("consoleLogRead", {
filter: this.filter
filter: this.filter,
read_size: this.read_size
}, (function(_this) {
return function(res) {
var pos_diff, size_read, size_total;
@ -202,11 +244,17 @@
pos_diff = res["pos_end"] - res["pos_start"];
size_read = Math.round(pos_diff / 1024);
size_total = Math.round(res['pos_end'] / 1024);
_this.text.append("<br><br>");
_this.text.append("Displaying " + res.lines.length + " of " + res.num_found + " lines found in the last " + size_read + "kB of the log file. (" + size_total + "kB total)<br>");
_this.addLines(res.lines, false);
return _this.text_elem.scrollTop = _this.text_elem.scrollHeight;
};
})(this));
if (this.stream_id) {
this.sidebar.wrapper.ws.cmd("consoleLogStreamRemove", {
stream_id: this.stream_id
});
}
return this.sidebar.wrapper.ws.cmd("consoleLogStream", {
filter: this.filter
}, (function(_this) {
@ -217,15 +265,17 @@
};
Console.prototype.close = function() {
window.top.location.hash = "";
this.sidebar.move_lock = "y";
this.sidebar.startDrag();
return this.sidebar.stopDrag();
};
Console.prototype.open = function() {
this.createHtmltag();
this.sidebar.fixbutton_targety = this.sidebar.page_height;
return this.stopDragY();
this.sidebar.startDrag();
this.sidebar.moved("y");
this.sidebar.fixbutton_targety = this.sidebar.page_height - this.sidebar.fixbutton_inity - 50;
return this.sidebar.stopDrag();
};
Console.prototype.onOpened = function() {
@ -276,6 +326,26 @@
}
};
Console.prototype.changeFilter = function(filter) {
this.filter = filter;
if (this.filter === "") {
this.read_size = 32 * 1024;
} else {
this.read_size = 5 * 1024 * 1024;
}
return this.loadConsoleText();
};
Console.prototype.handleTabClick = function(e) {
var elem;
elem = $(e.currentTarget);
this.tab_active = elem.data("filter");
$("a", this.tabs).removeClass("active");
elem.addClass("active");
this.changeFilter(this.tab_active);
return false;
};
return Console;
})(Class);
@ -285,7 +355,7 @@
}).call(this);
/* ---- plugins/Sidebar/media/Menu.coffee ---- */
/* ---- Menu.coffee ---- */
(function() {
@ -368,7 +438,7 @@
}).call(this);
/* ---- plugins/Sidebar/media/RateLimit.coffee ---- */
/* ---- RateLimit.coffee ---- */
(function() {
@ -397,7 +467,7 @@
}).call(this);
/* ---- plugins/Sidebar/media/Scrollable.js ---- */
/* ---- Scrollable.js ---- */
/* via http://jsfiddle.net/elGrecode/00dgurnn/ */
@ -492,7 +562,7 @@ window.initScrollable = function () {
return updateHeight;
};
/* ---- plugins/Sidebar/media/Sidebar.coffee ---- */
/* ---- Sidebar.coffee ---- */
(function() {
@ -534,9 +604,9 @@ window.initScrollable = function () {
this.globe = null;
this.preload_html = null;
this.original_set_site_info = this.wrapper.setSiteInfo;
if (false) {
if (window.top.location.hash === "#ZeroNet:OpenSidebar") {
this.startDrag();
this.moved();
this.moved("x");
this.fixbutton_targetx = this.fixbutton_initx - this.width;
this.stopDrag();
}
@ -744,9 +814,9 @@ window.initScrollable = function () {
return false;
};
})(this));
return this.tag.find("#privatekey-forgot").off("click, touchend").on("click touchend", (function(_this) {
return this.tag.find("#privatekey-forget").off("click, touchend").on("click touchend", (function(_this) {
return function(e) {
_this.wrapper.displayConfirm("Remove saved private key for this site?", "Forgot", function(res) {
_this.wrapper.displayConfirm("Remove saved private key for this site?", "Forget", function(res) {
if (!res) {
return false;
}
@ -946,6 +1016,18 @@ window.initScrollable = function () {
return false;
};
})(this));
this.tag.find("#button-autodownload_previous").off("click touchend").on("click touchend", (function(_this) {
return function() {
_this.wrapper.ws.cmd("siteUpdate", {
"address": _this.wrapper.site_info.address,
"check_files": true
}, function() {
return _this.wrapper.notifications.add("done-download_optional", "done", "Optional files downloaded", 5000);
});
_this.wrapper.notifications.add("start-download_optional", "info", "Optional files download started", 5000);
return false;
};
})(this));
this.tag.find("#button-dbreload").off("click touchend").on("click touchend", (function(_this) {
return function() {
_this.wrapper.ws.cmd("dbReload", [], function() {
@ -1279,7 +1361,7 @@ window.initScrollable = function () {
}).call(this);
/* ---- plugins/Sidebar/media/morphdom.js ---- */
/* ---- morphdom.js ---- */
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.morphdom = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){

View file

@ -0,0 +1,5 @@
{
"name": "Sidebar",
"description": "Access site management sidebar and console by dragging top-right 0 button to left or down.",
"default": "enabled"
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,5 @@
{
"name": "Stats",
"description": "/Stats and /Benchmark pages.",
"default": "enabled"
}

View file

@ -25,6 +25,8 @@ class UiRequestPlugin(object):
file_generator = super(UiRequestPlugin, self).actionSiteMedia(path, **kwargs)
if "__next__" in dir(file_generator): # File found and generator returned
site = self.server.sites.get(path_parts["address"])
if not site or not site.content_manager.contents.get("content.json"):
return file_generator
return self.actionPatchFile(site, path_parts["inner_path"], file_generator)
else:
return file_generator
@ -44,7 +46,7 @@ class UiRequestPlugin(object):
return file_generator
def actionPatchFile(self, site, inner_path, file_generator):
content_json = site.content_manager.contents["content.json"]
content_json = site.content_manager.contents.get("content.json")
lang_file = "languages/%s.json" % translate.lang
lang_file_exist = False
if site.settings.get("own"): # My site, check if the file is exist (allow to add new lang without signing)

View file

@ -0,0 +1,5 @@
{
"name": "TranslateSite",
"description": "Transparent support translation of site javascript and html files.",
"default": "enabled"
}

View file

@ -8,8 +8,11 @@ from Translate import Translate
allow_reload = False # No source reload supported in this plugin
plugin_dir = os.path.dirname(__file__)
if "_" not in locals():
_ = Translate("plugins/Trayicon/languages/")
_ = Translate(plugin_dir + "/languages/")
@PluginManager.registerTo("Actions")
@ -23,25 +26,20 @@ class ActionsPlugin(object):
self.main = main
fs_encoding = sys.getfilesystemencoding()
icon = notificationicon.NotificationIcon(
os.path.join(os.path.dirname(os.path.abspath(__file__)), 'trayicon.ico'),
"ZeroNet %s" % config.version
)
self.icon = icon
if not config.debug: # Hide console if not in debug mode
notificationicon.hideConsole()
self.console = False
else:
self.console = True
@atexit.register
def hideIcon():
if not config.debug:
notificationicon.showConsole()
try:
icon.die()
except Exception as err:
print("Error removing trayicon: %s" % err)
ui_ip = config.ui_ip if config.ui_ip != "*" else "127.0.0.1"
@ -134,25 +132,30 @@ class ActionsPlugin(object):
else:
cwd = os.path.dirname(sys.executable)
ignored_args = [
"--open_browser", "default_browser",
"--dist_type", "bundle_win64"
]
if sys.platform == 'win32':
args = ['"%s"' % arg for arg in args if arg]
args = ['"%s"' % arg for arg in args if arg and arg not in ignored_args]
cmd = " ".join(args)
# Dont open browser on autorun
cmd = cmd.replace("start.py", "zeronet.py").replace('"--open_browser"', "").replace('"default_browser"', "").strip()
cmd = cmd.replace("start.py", "zeronet.py").strip()
cmd += ' --open_browser ""'
return """
@echo off
chcp 65001 > nul
set PYTHONIOENCODING=utf-8
cd /D \"%s\"
start "" %s
""" % (cwd, cmd)
return "\r\n".join([
'@echo off',
'chcp 65001 > nul',
'set PYTHONIOENCODING=utf-8',
'cd /D \"%s\"' % cwd,
'start "" %s' % cmd
])
def isAutorunEnabled(self):
path = self.getAutorunPath()
return os.path.isfile(path) and open(path).read() == self.formatAutorun()
return os.path.isfile(path) and open(path, "rb").read().decode("utf8") == self.formatAutorun()
def titleAutorun(self):
translate = _["Start ZeroNet when Windows starts"]
@ -165,4 +168,4 @@ class ActionsPlugin(object):
if self.isAutorunEnabled():
os.unlink(self.getAutorunPath())
else:
open(self.getAutorunPath(), "w").write(self.formatAutorun())
open(self.getAutorunPath(), "wb").write(self.formatAutorun().encode("utf8"))

View file

@ -0,0 +1,14 @@
{
"ZeroNet Twitter": "ZeroNet Twitter",
"ZeroNet Reddit": "ZeroNet Reddit",
"ZeroNet Github": "ZeroNet Github",
"Report bug/request feature": "Zgłoś błąd / propozycję",
"!Open ZeroNet": "!Otwórz ZeroNet",
"Quit": "Zamknij",
"(active)": "(aktywny)",
"(passive)": "(pasywny)",
"Connections: %s": "Połączenia: %s",
"Received: %.2f MB | Sent: %.2f MB": "Odebrano: %.2f MB | Wysłano: %.2f MB",
"Show console window": "Pokaż okno konsoli",
"Start ZeroNet when Windows starts": "Uruchom ZeroNet podczas startu Windows"
}

View file

@ -0,0 +1,5 @@
{
"name": "Trayicon",
"description": "Icon for system tray. (Windows only)",
"default": "enabled"
}

View file

@ -1,18 +1,16 @@
import io
import os
from Plugin import PluginManager
from Config import config
from Translate import Translate
from util.Flag import flag
plugin_dir = os.path.dirname(__file__)
if "_" not in locals():
_ = Translate("plugins/UiConfig/languages/")
@PluginManager.afterLoad
def importPluginnedClasses():
from Ui import UiWebsocket
UiWebsocket.admin_commands.add("configList")
_ = Translate(plugin_dir + "/languages/")
@PluginManager.registerTo("UiRequest")
@ -35,7 +33,7 @@ class UiRequestPlugin(object):
def actionUiMedia(self, path, *args, **kwargs):
if path.startswith("/uimedia/plugins/uiconfig/"):
file_path = path.replace("/uimedia/plugins/uiconfig/", "plugins/UiConfig/media/")
file_path = path.replace("/uimedia/plugins/uiconfig/", plugin_dir + "/media/")
if config.debug and (file_path.endswith("all.js") or file_path.endswith("all.css")):
# If debugging merge *.css to all.css and *.js to all.js
from Debug import DebugMedia
@ -55,6 +53,7 @@ class UiRequestPlugin(object):
@PluginManager.registerTo("UiWebsocket")
class UiWebsocketPlugin(object):
@flag.admin
def actionConfigList(self, to):
back = {}
config_values = vars(config.arguments)

View file

@ -0,0 +1,62 @@
{
"ZeroNet config": "Konfiguracja ZeroNet",
"Web Interface": "Interfejs webowy",
"Open web browser on ZeroNet startup": "Otwórz przeglądarkę podczas uruchomienia ZeroNet",
"Network": "Sieć",
"Offline mode": "Tryb offline",
"Disable network communication.": "Wyłącz komunikacje sieciową",
"File server network": "Sieć serwera plików",
"Accept incoming peers using IPv4 or IPv6 address. (default: dual)": "Akceptuj połączenia przychodzące używając IPv4 i IPv6. (domyślnie: oba)",
"Dual (IPv4 & IPv6)": "Oba (IPv4 i IPv6)",
"File server port": "Port serwera plików",
"Other peers will use this port to reach your served sites. (default: 15441)": "Inni użytkownicy będą używać tego portu do połączenia się z Tobą. (domyślnie 15441)",
"File server external ip": "Zewnętrzny adres IP serwera plików",
"Detect automatically": "Wykryj automatycznie",
"Your file server is accessible on these ips. (default: detect automatically)": "Twój serwer plików będzie dostępny na tych adresach IP. (domyślnie: wykryj automatycznie)",
"Disable: Don't connect to peers on Tor network": "Wyłącz: Nie łącz się do użytkowników sieci Tor",
"Enable: Only use Tor for Tor network peers": "Włącz: Łącz się do użytkowników sieci Tor",
"Always: Use Tor for every connections to hide your IP address (slower)": "Zawsze Tor: Użyj Tor dla wszystkich połączeń w celu ukrycia Twojego adresu IP (wolne działanie)",
"Disable": "Wyłącz",
"Enable": "Włącz",
"Always": "Zawsze Tor",
"Use Tor bridges": "Użyj Tor bridges",
"Use obfuscated bridge relays to avoid network level Tor block (even slower)": "Użyj obfuskacji, aby uniknąć blokowania Tor na poziomie sieci (jeszcze wolniejsze działanie)",
"Trackers": "Trackery",
"Discover new peers using these adresses": "Wykryj użytkowników korzystając z tych adresów trackerów",
"Trackers files": "Pliki trackerów",
"Load additional list of torrent trackers dynamically, from a file": "Dynamicznie wczytaj dodatkową listę trackerów z pliku .json",
"Eg.: data/trackers.json": "Np.: data/trackers.json",
"Proxy for tracker connections": "Serwer proxy dla trackerów",
"Custom": "Własny",
"Custom socks proxy address for trackers": "Adres serwera proxy do łączenia z trackerami",
"Eg.: 127.0.0.1:1080": "Np.: 127.0.0.1:1080",
"Performance": "Wydajność",
"Level of logging to file": "Poziom logowania do pliku",
"Everything": "Wszystko",
"Only important messages": "Tylko ważne wiadomości",
"Only errors": "Tylko błędy",
"Threads for async file system reads": "Wątki asynchroniczne dla odczytu",
"Threads for async file system writes": "Wątki asynchroniczne dla zapisu",
"Threads for cryptographic functions": "Wątki dla funkcji kryptograficznych",
"Threads for database operations": "Wątki dla operacji na bazie danych",
"Sync read": "Synchroniczny odczyt",
"Sync write": "Synchroniczny zapis",
"Sync execution": "Synchroniczne wykonanie",
"1 thread": "1 wątek",
"2 threads": "2 wątki",
"3 threads": "3 wątki",
"4 threads": "4 wątki",
"5 threads": "5 wątków",
"10 threads": "10 wątków",
" configuration item value changed": " obiekt konfiguracji zmieniony",
"Save settings": "Zapisz ustawienia",
"Some changed settings requires restart": "Niektóre zmiany ustawień wymagają ponownego uruchomienia ZeroNet",
"Restart ZeroNet client": "Uruchom ponownie ZeroNet"
}

View file

@ -13,7 +13,7 @@
"Disable": "Desativar",
"Enable": "Ativar",
"Sempre": "Mindig",
"Always": "Sempre",
"Use Tor bridges": "Usar pontes do Tor",
"Use obfuscated bridge relays to avoid network level Tor block (even slower)": "Usar relays de ponte ofuscados para evitar o bloqueio de Tor de nível de rede (ainda mais lento)",
@ -29,5 +29,28 @@
"configuration item value changed": " valor do item de configuração foi alterado",
"Save settings": "Salvar configurações",
"Some changed settings requires restart": "Algumas configurações alteradas requrem reinicialização",
"Restart ZeroNet client": "Reiniciar cliente da ZeroNet"
"Restart ZeroNet client": "Reiniciar cliente da ZeroNet",
"Offline mode": "Modo offline",
"Disable network communication.": "Desativar a comunicação em rede.",
"File server network": "Rede do servidor de arquivos",
"Accept incoming peers using IPv4 or IPv6 address. (default: dual)": "Aceite pontos de entrada usando o endereço IPv4 ou IPv6. (padrão: dual)",
"File server external ip": "IP externo do servidor de arquivos",
"Performance": "Desempenho",
"Level of logging to file": "Nível de registro no arquivo",
"Everything": "Tudo",
"Only important messages": "Apenas mensagens importantes",
"Only errors": "Apenas erros",
"Threads for async file system reads": "Threads para leituras de sistema de arquivos assíncronas",
"Threads for async file system writes": "Threads para gravações do sistema de arquivos assíncrono",
"Threads for cryptographic functions": "Threads para funções criptográficas",
"Threads for database operations": "Threads para operações de banco de dados",
"Sync execution": "Execução de sincronização",
"Custom": "Personalizado",
"Custom socks proxy address for trackers": "Endereço de proxy de meias personalizadas para trackers",
"Your file server is accessible on these ips. (default: detect automatically)": "Seu servidor de arquivos está acessível nesses ips. (padrão: detectar automaticamente)",
"Detect automatically": "Detectar automaticamente",
" configuration item value changed": " valor do item de configuração alterado"
}

View file

@ -41,6 +41,19 @@
"Everything": "记录全部",
"Only important messages": "仅记录重要信息",
"Only errors": "仅记录错误",
"Threads for async file system reads": "文件系统异步读取线程",
"Threads for async file system writes": "文件系统异步写入线程",
"Threads for cryptographic functions": "密码函数线程",
"Threads for database operations": "数据库操作线程",
"Sync read": "同步读取",
"Sync write": "同步写入",
"Sync execution": "同步执行",
"1 thread": "1个线程",
"2 threads": "2个线程",
"3 threads": "3个线程",
"4 threads": "4个线程",
"5 threads": "5个线程",
"10 threads": "10个线程",
" configuration item value changed": " 个配置值已经改变",
"Save settings": "保存配置",

View file

@ -1,8 +1,8 @@
body { background-color: #EDF2F5; font-family: Roboto, 'Segoe UI', Arial, 'Helvetica Neue'; margin: 0px; padding: 0px; backface-visibility: hidden; }
h1, h2, h3, h4 { font-family: 'Roboto', Arial, sans-serif; font-weight: 200; font-size: 30px; margin: 0px; padding: 0px }
h1 { background: linear-gradient(33deg,#af3bff,#0d99c9); color: white; padding: 16px 30px; }
h2 { margin-top: 10px; }
h3 { font-weight: normal }
h1 { background: linear-gradient(33deg,#af3bff,#0d99c9); color: white; padding: 16px 30px; }
a { color: #9760F9 }
a:hover { text-decoration: none }
@ -53,7 +53,7 @@ a:hover { text-decoration: none }
/* Bottom */
.bottom {
width: 100%; text-align: center; background-color: #ffffffde; padding: 25px; bottom: -120px;
width: 100%; text-align: center; background-color: #ffffffde; padding: 25px; bottom: -120px; -webkit-backdrop-filter: blur(5px); backdrop-filter: blur(5px);
transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); position: fixed; backface-visibility: hidden; box-sizing: border-box;
}
.bottom-content { max-width: 750px; width: 100%; margin: 0px auto; }

View file

@ -1,13 +1,12 @@
/* ---- plugins/UiConfig/media/css/Config.css ---- */
/* ---- Config.css ---- */
body { background-color: #EDF2F5; font-family: Roboto, 'Segoe UI', Arial, 'Helvetica Neue'; margin: 0px; padding: 0px; -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -o-backface-visibility: hidden; -ms-backface-visibility: hidden; backface-visibility: hidden ; }
h1, h2, h3, h4 { font-family: 'Roboto', Arial, sans-serif; font-weight: 200; font-size: 30px; margin: 0px; padding: 0px }
h1 { background: -webkit-linear-gradient(33deg,#af3bff,#0d99c9);background: -moz-linear-gradient(33deg,#af3bff,#0d99c9);background: -o-linear-gradient(33deg,#af3bff,#0d99c9);background: -ms-linear-gradient(33deg,#af3bff,#0d99c9);background: linear-gradient(33deg,#af3bff,#0d99c9); color: white; padding: 16px 30px; }
h2 { margin-top: 10px; }
h3 { font-weight: normal }
h1 { background: -webkit-linear-gradient(33deg,#af3bff,#0d99c9);background: -moz-linear-gradient(33deg,#af3bff,#0d99c9);background: -o-linear-gradient(33deg,#af3bff,#0d99c9);background: -ms-linear-gradient(33deg,#af3bff,#0d99c9);background: linear-gradient(33deg,#af3bff,#0d99c9); color: white; padding: 16px 30px; }
a { color: #9760F9 }
a:hover { text-decoration: none }
@ -58,7 +57,7 @@ a:hover { text-decoration: none }
/* Bottom */
.bottom {
width: 100%; text-align: center; background-color: #ffffffde; padding: 25px; bottom: -120px;
width: 100%; text-align: center; background-color: #ffffffde; padding: 25px; bottom: -120px; -webkit-backdrop-filter: blur(5px); backdrop-filter: blur(5px);
-webkit-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); -moz-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); -o-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); -ms-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1) ; position: fixed; -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -o-backface-visibility: hidden; -ms-backface-visibility: hidden; backface-visibility: hidden ; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ;
}
.bottom-content { max-width: 750px; width: 100%; margin: 0px auto; }
@ -73,7 +72,7 @@ a:hover { text-decoration: none }
.animate-inout { -webkit-transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; -moz-transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; -o-transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; -ms-transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important ; }
/* ---- plugins/UiConfig/media/css/button.css ---- */
/* ---- button.css ---- */
/* Button */
@ -90,7 +89,7 @@ a:hover { text-decoration: none }
.button.disabled { color: #DDD; background-color: #999; pointer-events: none; border-bottom: 2px solid #666 }
/* ---- plugins/UiConfig/media/css/fonts.css ---- */
/* ---- fonts.css ---- */
/* Base64 encoder: http://www.motobit.com/util/base64-decoder-encoder.asp */

View file

@ -109,7 +109,7 @@ class ConfigStorage extends Class
section.items.push
title: "Trackers files"
key: "trackers_file"
type: "text"
type: "textarea"
description: "Load additional list of torrent trackers dynamically, from a file"
placeholder: "Eg.: data/trackers.json"
value_pos: "fullwidth"
@ -123,6 +123,8 @@ class ConfigStorage extends Class
{title: "Tor", value: "tor"}
{title: "Disable", value: "disable"}
]
isHidden: ->
Page.values["tor"] == "always"
section.items.push
title: "Custom socks proxy address for trackers"
@ -147,6 +149,62 @@ class ConfigStorage extends Class
{title: "Only errors", value: "ERROR"}
]
section.items.push
key: "threads_fs_read"
title: "Threads for async file system reads"
type: "select"
options: [
{title: "Sync read", value: 0}
{title: "1 thread", value: 1}
{title: "2 threads", value: 2}
{title: "3 threads", value: 3}
{title: "4 threads", value: 4}
{title: "5 threads", value: 5}
{title: "10 threads", value: 10}
]
section.items.push
key: "threads_fs_write"
title: "Threads for async file system writes"
type: "select"
options: [
{title: "Sync write", value: 0}
{title: "1 thread", value: 1}
{title: "2 threads", value: 2}
{title: "3 threads", value: 3}
{title: "4 threads", value: 4}
{title: "5 threads", value: 5}
{title: "10 threads", value: 10}
]
section.items.push
key: "threads_crypt"
title: "Threads for cryptographic functions"
type: "select"
options: [
{title: "Sync execution", value: 0}
{title: "1 thread", value: 1}
{title: "2 threads", value: 2}
{title: "3 threads", value: 3}
{title: "4 threads", value: 4}
{title: "5 threads", value: 5}
{title: "10 threads", value: 10}
]
section.items.push
key: "threads_db"
title: "Threads for database operations"
type: "select"
options: [
{title: "Sync execution", value: 0}
{title: "1 thread", value: 1}
{title: "2 threads", value: 2}
{title: "3 threads", value: 3}
{title: "4 threads", value: 4}
{title: "5 threads", value: 5}
{title: "10 threads", value: 10}
]
createSection: (title) =>
section = {}
section.title = title

View file

@ -118,7 +118,7 @@ class ConfigView extends Class
renderValueSelect: (item) =>
h("select.input-select", {config_key: item.key, oninput: @handleInputChange},
item.options.map (option) =>
h("option", {selected: option.value == @values[item.key], value: option.value}, option.title)
h("option", {selected: option.value.toString() == @values[item.key], value: option.value}, option.title)
)
window.ConfigView = ConfigView

View file

@ -1,5 +1,5 @@
/* ---- plugins/UiConfig/media/js/lib/Class.coffee ---- */
/* ---- lib/Class.coffee ---- */
(function() {
@ -55,8 +55,7 @@
}).call(this);
/* ---- plugins/UiConfig/media/js/lib/Promise.coffee ---- */
/* ---- lib/Promise.coffee ---- */
(function() {
@ -159,8 +158,7 @@
}).call(this);
/* ---- plugins/UiConfig/media/js/lib/Prototypes.coffee ---- */
/* ---- lib/Prototypes.coffee ---- */
(function() {
@ -186,8 +184,7 @@
}).call(this);
/* ---- plugins/UiConfig/media/js/lib/maquette.js ---- */
/* ---- lib/maquette.js ---- */
(function (root, factory) {
@ -962,7 +959,7 @@
}));
/* ---- plugins/UiConfig/media/js/utils/Animation.coffee ---- */
/* ---- utils/Animation.coffee ---- */
(function() {
@ -1128,8 +1125,7 @@
}).call(this);
/* ---- plugins/UiConfig/media/js/utils/Dollar.coffee ---- */
/* ---- utils/Dollar.coffee ---- */
(function() {
@ -1141,8 +1137,7 @@
}).call(this);
/* ---- plugins/UiConfig/media/js/utils/ZeroFrame.coffee ---- */
/* ---- utils/ZeroFrame.coffee ---- */
(function() {
@ -1273,8 +1268,7 @@
}).call(this);
/* ---- plugins/UiConfig/media/js/ConfigStorage.coffee ---- */
/* ---- ConfigStorage.coffee ---- */
(function() {
@ -1430,7 +1424,7 @@
section.items.push({
title: "Trackers files",
key: "trackers_file",
type: "text",
type: "textarea",
description: "Load additional list of torrent trackers dynamically, from a file",
placeholder: "Eg.: data/trackers.json",
value_pos: "fullwidth"
@ -1450,7 +1444,10 @@
title: "Disable",
value: "disable"
}
]
],
isHidden: function() {
return Page.values["tor"] === "always";
}
});
section.items.push({
title: "Custom socks proxy address for trackers",
@ -1467,7 +1464,7 @@
})(this)
});
section = this.createSection("Performance");
return section.items.push({
section.items.push({
key: "log_level",
title: "Level of logging to file",
type: "select",
@ -1484,6 +1481,122 @@
}
]
});
section.items.push({
key: "threads_fs_read",
title: "Threads for async file system reads",
type: "select",
options: [
{
title: "Sync read",
value: 0
}, {
title: "1 thread",
value: 1
}, {
title: "2 threads",
value: 2
}, {
title: "3 threads",
value: 3
}, {
title: "4 threads",
value: 4
}, {
title: "5 threads",
value: 5
}, {
title: "10 threads",
value: 10
}
]
});
section.items.push({
key: "threads_fs_write",
title: "Threads for async file system writes",
type: "select",
options: [
{
title: "Sync write",
value: 0
}, {
title: "1 thread",
value: 1
}, {
title: "2 threads",
value: 2
}, {
title: "3 threads",
value: 3
}, {
title: "4 threads",
value: 4
}, {
title: "5 threads",
value: 5
}, {
title: "10 threads",
value: 10
}
]
});
section.items.push({
key: "threads_crypt",
title: "Threads for cryptographic functions",
type: "select",
options: [
{
title: "Sync execution",
value: 0
}, {
title: "1 thread",
value: 1
}, {
title: "2 threads",
value: 2
}, {
title: "3 threads",
value: 3
}, {
title: "4 threads",
value: 4
}, {
title: "5 threads",
value: 5
}, {
title: "10 threads",
value: 10
}
]
});
return section.items.push({
key: "threads_db",
title: "Threads for database operations",
type: "select",
options: [
{
title: "Sync execution",
value: 0
}, {
title: "1 thread",
value: 1
}, {
title: "2 threads",
value: 2
}, {
title: "3 threads",
value: 3
}, {
title: "4 threads",
value: 4
}, {
title: "5 threads",
value: 5
}, {
title: "10 threads",
value: 10
}
]
});
};
ConfigStorage.prototype.createSection = function(title) {
@ -1504,7 +1617,7 @@
}).call(this);
/* ---- plugins/UiConfig/media/js/ConfigView.coffee ---- */
/* ---- ConfigView.coffee ---- */
(function() {
@ -1692,7 +1805,7 @@
}, item.options.map((function(_this) {
return function(option) {
return h("option", {
selected: option.value === _this.values[item.key],
selected: option.value.toString() === _this.values[item.key],
value: option.value
}, option.title);
};
@ -1707,8 +1820,7 @@
}).call(this);
/* ---- plugins/UiConfig/media/js/UiConfig.coffee ---- */
/* ---- UiConfig.coffee ---- */
(function() {

View file

@ -0,0 +1,5 @@
{
"name": "UiConfig",
"description": "Change client settings using the web interface.",
"default": "enabled"
}

View file

@ -0,0 +1,221 @@
import io
import os
import json
import shutil
import time
from Plugin import PluginManager
from Config import config
from Debug import Debug
from Translate import Translate
from util.Flag import flag
plugin_dir = os.path.dirname(__file__)
if "_" not in locals():
_ = Translate(plugin_dir + "/languages/")
# Convert non-str,int,float values to str in a dict
def restrictDictValues(input_dict):
allowed_types = (int, str, float)
return {
key: val if type(val) in allowed_types else str(val)
for key, val in input_dict.items()
}
@PluginManager.registerTo("UiRequest")
class UiRequestPlugin(object):
def actionWrapper(self, path, extra_headers=None):
if path.strip("/") != "Plugins":
return super(UiRequestPlugin, self).actionWrapper(path, extra_headers)
if not extra_headers:
extra_headers = {}
script_nonce = self.getScriptNonce()
self.sendHeader(extra_headers=extra_headers, script_nonce=script_nonce)
site = self.server.site_manager.get(config.homepage)
return iter([super(UiRequestPlugin, self).renderWrapper(
site, path, "uimedia/plugins/plugin_manager/plugin_manager.html",
"Plugin Manager", extra_headers, show_loadingscreen=False, script_nonce=script_nonce
)])
def actionUiMedia(self, path, *args, **kwargs):
if path.startswith("/uimedia/plugins/plugin_manager/"):
file_path = path.replace("/uimedia/plugins/plugin_manager/", plugin_dir + "/media/")
if config.debug and (file_path.endswith("all.js") or file_path.endswith("all.css")):
# If debugging merge *.css to all.css and *.js to all.js
from Debug import DebugMedia
DebugMedia.merge(file_path)
if file_path.endswith("js"):
data = _.translateData(open(file_path).read(), mode="js").encode("utf8")
elif file_path.endswith("html"):
data = _.translateData(open(file_path).read(), mode="html").encode("utf8")
else:
data = open(file_path, "rb").read()
return self.actionFile(file_path, file_obj=io.BytesIO(data), file_size=len(data))
else:
return super(UiRequestPlugin, self).actionUiMedia(path)
@PluginManager.registerTo("UiWebsocket")
class UiWebsocketPlugin(object):
@flag.admin
def actionPluginList(self, to):
plugins = []
for plugin in PluginManager.plugin_manager.listPlugins(list_disabled=True):
plugin_info_path = plugin["dir_path"] + "/plugin_info.json"
plugin_info = {}
if os.path.isfile(plugin_info_path):
try:
plugin_info = json.load(open(plugin_info_path))
except Exception as err:
self.log.error(
"Error loading plugin info for %s: %s" %
(plugin["name"], Debug.formatException(err))
)
if plugin_info:
plugin_info = restrictDictValues(plugin_info) # For security reasons don't allow complex values
plugin["info"] = plugin_info
if plugin["source"] != "builtin":
plugin_site = self.server.sites.get(plugin["source"])
if plugin_site:
try:
plugin_site_info = plugin_site.storage.loadJson(plugin["inner_path"] + "/plugin_info.json")
plugin_site_info = restrictDictValues(plugin_site_info)
plugin["site_info"] = plugin_site_info
plugin["site_title"] = plugin_site.content_manager.contents["content.json"].get("title")
plugin_key = "%s/%s" % (plugin["source"], plugin["inner_path"])
plugin["updated"] = plugin_key in PluginManager.plugin_manager.plugins_updated
except Exception:
pass
plugins.append(plugin)
return {"plugins": plugins}
@flag.admin
@flag.no_multiuser
def actionPluginConfigSet(self, to, source, inner_path, key, value):
plugin_manager = PluginManager.plugin_manager
plugins = plugin_manager.listPlugins(list_disabled=True)
plugin = None
for item in plugins:
if item["source"] == source and item["inner_path"] in (inner_path, "disabled-" + inner_path):
plugin = item
break
if not plugin:
return {"error": "Plugin not found"}
config_source = plugin_manager.config.setdefault(source, {})
config_plugin = config_source.setdefault(inner_path, {})
if key in config_plugin and value is None:
del config_plugin[key]
else:
config_plugin[key] = value
plugin_manager.saveConfig()
return "ok"
def pluginAction(self, action, address, inner_path):
site = self.server.sites.get(address)
plugin_manager = PluginManager.plugin_manager
# Install/update path should exists
if action in ("add", "update", "add_request"):
if not site:
raise Exception("Site not found")
if not site.storage.isDir(inner_path):
raise Exception("Directory not found on the site")
try:
plugin_info = site.storage.loadJson(inner_path + "/plugin_info.json")
plugin_data = (plugin_info["rev"], plugin_info["description"], plugin_info["name"])
except Exception as err:
raise Exception("Invalid plugin_info.json: %s" % Debug.formatExceptionMessage(err))
source_path = site.storage.getPath(inner_path)
target_path = plugin_manager.path_installed_plugins + "/" + address + "/" + inner_path
plugin_config = plugin_manager.config.setdefault(site.address, {}).setdefault(inner_path, {})
# Make sure plugin (not)installed
if action in ("add", "add_request") and os.path.isdir(target_path):
raise Exception("Plugin already installed")
if action in ("update", "remove") and not os.path.isdir(target_path):
raise Exception("Plugin not installed")
# Do actions
if action == "add":
shutil.copytree(source_path, target_path)
plugin_config["date_added"] = int(time.time())
plugin_config["rev"] = plugin_info["rev"]
plugin_config["enabled"] = True
if action == "update":
shutil.rmtree(target_path)
shutil.copytree(source_path, target_path)
plugin_config["rev"] = plugin_info["rev"]
plugin_config["date_updated"] = time.time()
if action == "remove":
del plugin_manager.config[address][inner_path]
shutil.rmtree(target_path)
def doPluginAdd(self, to, inner_path, res):
if not res:
return None
self.pluginAction("add", self.site.address, inner_path)
PluginManager.plugin_manager.saveConfig()
self.cmd(
"confirm",
["Plugin installed!<br>You have to restart the client to load the plugin", "Restart"],
lambda res: self.actionServerShutdown(to, restart=True)
)
self.response(to, "ok")
@flag.no_multiuser
def actionPluginAddRequest(self, to, inner_path):
self.pluginAction("add_request", self.site.address, inner_path)
plugin_info = self.site.storage.loadJson(inner_path + "/plugin_info.json")
warning = "<b>Warning!<br/>Plugins has the same permissions as the ZeroNet client.<br/>"
warning += "Do not install it if you don't trust the developer.</b>"
self.cmd(
"confirm",
["Install new plugin: %s?<br>%s" % (plugin_info["name"], warning), "Trust & Install"],
lambda res: self.doPluginAdd(to, inner_path, res)
)
@flag.admin
@flag.no_multiuser
def actionPluginRemove(self, to, address, inner_path):
self.pluginAction("remove", address, inner_path)
PluginManager.plugin_manager.saveConfig()
return "ok"
@flag.admin
@flag.no_multiuser
def actionPluginUpdate(self, to, address, inner_path):
self.pluginAction("update", address, inner_path)
PluginManager.plugin_manager.saveConfig()
PluginManager.plugin_manager.plugins_updated["%s/%s" % (address, inner_path)] = True
return "ok"

View file

@ -0,0 +1 @@
from . import UiPluginManagerPlugin

View file

@ -0,0 +1,75 @@
body { background-color: #EDF2F5; font-family: Roboto, 'Segoe UI', Arial, 'Helvetica Neue'; margin: 0px; padding: 0px; backface-visibility: hidden; }
h1, h2, h3, h4 { font-family: 'Roboto', Arial, sans-serif; font-weight: 200; font-size: 30px; margin: 0px; padding: 0px }
h1 { background: linear-gradient(33deg,#af3bff,#0d99c9); color: white; padding: 16px 30px; }
h2 { margin-top: 10px; }
h3 { font-weight: normal }
h4 { font-size: 19px; font-weight: lighter; margin-right: 100px; margin-top: 30px; }
a { color: #9760F9 }
a:hover { text-decoration: none }
.link { background-color: transparent; outline: 5px solid transparent; transition: all 0.3s }
.link:active { background-color: #EFEFEF; outline: 5px solid #EFEFEF; transition: none }
.content { max-width: 800px; margin: auto; background-color: white; padding: 60px 20px; box-sizing: border-box; padding-bottom: 150px; }
.section { margin: 0px 10%; }
.plugins { font-size: 19px; margin-top: 25px; margin-bottom: 75px; }
.plugin { transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); position: relative; padding-bottom: 20px; padding-top: 10px; }
.plugin.hidden { opacity: 0; height: 0px; padding: 0px; }
.plugin .title { display: inline-block; line-height: 36px; }
.plugin .title h3 { font-size: 20px; font-weight: lighter; margin-right: 100px; }
.plugin .title .version { font-size: 70%; margin-left: 5px; }
.plugin .title .version .version-latest { color: #2ecc71; font-weight: normal; }
.plugin .title .version .version-missing { color: #ffa200; font-weight: normal; }
.plugin .title .version .version-update { padding: 0px 15px; margin-left: 5px; line-height: 28px; }
.plugin .description { font-size: 14px; color: #666; line-height: 24px; }
.plugin .description .source { color: #999; font-size: 90%; }
.plugin .description .source a { color: #666; }
.plugin .value { display: inline-block; white-space: nowrap; }
.plugin .value-right { right: 0px; position: absolute; }
.plugin .value-fullwidth { width: 100% }
.plugin .marker {
font-weight: bold; text-decoration: none; font-size: 25px; position: absolute; padding: 2px 15px; line-height: 32px;
opacity: 0; pointer-events: none; transition: all 0.6s; transform: scale(2); color: #9760F9;
}
.plugin .marker.visible { opacity: 1; pointer-events: all; transform: scale(1); }
.plugin .marker.changed { color: #2ecc71; }
.plugin .marker.pending { color: #ffa200; }
.input-text, .input-select { padding: 8px 18px; border: 1px solid #CCC; border-radius: 3px; font-size: 17px; box-sizing: border-box; }
.input-text:focus, .input-select:focus { border: 1px solid #3396ff; outline: none; }
.input-textarea { overflow-x: auto; overflow-y: hidden; white-space: pre; line-height: 22px; }
.input-select { width: initial; font-size: 14px; padding-right: 10px; padding-left: 10px; }
.value-right .input-text { text-align: right; width: 100px; }
.value-fullwidth .input-text { width: 100%; font-size: 14px; font-family: 'Segoe UI', Arial, 'Helvetica Neue'; }
.value-fullwidth { margin-top: 10px; }
/* Checkbox */
.checkbox-skin { background-color: #CCC; width: 50px; height: 24px; border-radius: 15px; transition: all 0.3s ease-in-out; display: inline-block; }
.checkbox-skin:before {
content: ""; position: relative; width: 20px; background-color: white; height: 20px; display: block; border-radius: 100%; margin-top: 2px; margin-left: 2px;
transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86);
}
.checkbox { font-size: 14px; font-weight: normal; display: inline-block; cursor: pointer; margin-top: 5px; }
.checkbox .title { display: inline; line-height: 30px; vertical-align: 4px; margin-left: 11px }
.checkbox.checked .checkbox-skin:before { margin-left: 27px; }
.checkbox.checked .checkbox-skin { background-color: #2ECC71 }
/* Bottom */
.bottom {
width: 100%; text-align: center; background-color: #ffffffde; padding: 25px; bottom: -120px; -webkit-backdrop-filter: blur(5px); backdrop-filter: blur(5px);
transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); position: fixed; backface-visibility: hidden; box-sizing: border-box;
}
.bottom-content { max-width: 750px; width: 100%; margin: 0px auto; }
.bottom .button { float: right; }
.bottom.visible { bottom: 0px; box-shadow: 0px 0px 35px #dcdcdc; }
.bottom .title { padding: 10px 10px; color: #363636; float: left; text-transform: uppercase; letter-spacing: 1px; }
.bottom .title:before { content: "•"; display: inline-block; color: #2ecc71; font-size: 31px; vertical-align: -7px; margin-right: 8px; line-height: 25px; }
.bottom-restart .title:before { color: #ffa200; }
.animate { transition: all 0.3s ease-out !important; }
.animate-back { transition: all 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; }
.animate-inout { transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; }

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,12 @@
/* Button */
.button {
background-color: #FFDC00; color: black; padding: 10px 20px; display: inline-block; background-position: left center;
border-radius: 2px; border-bottom: 2px solid #E8BE29; transition: all 0.5s ease-out; text-decoration: none;
}
.button:hover { border-color: white; border-bottom: 2px solid #BD960C; transition: none ; background-color: #FDEB07 }
.button:active { position: relative; top: 1px }
.button.loading {
color: rgba(0,0,0,0); background: #999 url(../img/loading.gif) no-repeat center center;
transition: all 0.5s ease-out ; pointer-events: none; border-bottom: 2px solid #666
}
.button.disabled { color: #DDD; background-color: #999; pointer-events: none; border-bottom: 2px solid #666 }

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 B

View file

@ -0,0 +1,132 @@
class PluginList extends Class
constructor: (plugins) ->
@plugins = plugins
savePluginStatus: (plugin, is_enabled) =>
Page.cmd "pluginConfigSet", [plugin.source, plugin.inner_path, "enabled", is_enabled], (res) =>
if res == "ok"
Page.updatePlugins()
else
Page.cmd "wrapperNotification", ["error", res.error]
Page.projector.scheduleRender()
handleCheckboxChange: (e) =>
node = e.currentTarget
plugin = node["data-plugin"]
node.classList.toggle("checked")
value = node.classList.contains("checked")
@savePluginStatus(plugin, value)
handleResetClick: (e) =>
node = e.currentTarget
plugin = node["data-plugin"]
@savePluginStatus(plugin, null)
handleUpdateClick: (e) =>
node = e.currentTarget
plugin = node["data-plugin"]
node.classList.add("loading")
Page.cmd "pluginUpdate", [plugin.source, plugin.inner_path], (res) =>
if res == "ok"
Page.cmd "wrapperNotification", ["done", "Plugin #{plugin.name} updated to latest version"]
Page.updatePlugins()
else
Page.cmd "wrapperNotification", ["error", res.error]
node.classList.remove("loading")
return false
handleDeleteClick: (e) =>
node = e.currentTarget
plugin = node["data-plugin"]
if plugin.loaded
Page.cmd "wrapperNotification", ["info", "You can only delete plugin that are not currently active"]
return false
node.classList.add("loading")
Page.cmd "wrapperConfirm", ["Delete #{plugin.name} plugin?", "Delete"], (res) =>
if not res
node.classList.remove("loading")
return false
Page.cmd "pluginRemove", [plugin.source, plugin.inner_path], (res) =>
if res == "ok"
Page.cmd "wrapperNotification", ["done", "Plugin #{plugin.name} deleted"]
Page.updatePlugins()
else
Page.cmd "wrapperNotification", ["error", res.error]
node.classList.remove("loading")
return false
render: ->
h("div.plugins", @plugins.map (plugin) =>
if not plugin.info
return
descr = plugin.info.description
plugin.info.default ?= "enabled"
if plugin.info.default
descr += " (default: #{plugin.info.default})"
tag_version = ""
tag_source = ""
tag_delete = ""
if plugin.source != "builtin"
tag_update = ""
if plugin.site_info?.rev
if plugin.site_info.rev > plugin.info.rev
tag_update = h("a.version-update.button",
{href: "#Update+plugin", onclick: @handleUpdateClick, "data-plugin": plugin},
"Update to rev#{plugin.site_info.rev}"
)
else
tag_update = h("span.version-missing", "(unable to get latest vesion: update site missing)")
tag_version = h("span.version",[
"rev#{plugin.info.rev} ",
tag_update,
])
tag_source = h("div.source",[
"Source: ",
h("a", {"href": "/#{plugin.source}", "target": "_top"}, if plugin.site_title then plugin.site_title else plugin.source),
" /" + plugin.inner_path
])
tag_delete = h("a.delete", {"href": "#Delete+plugin", onclick: @handleDeleteClick, "data-plugin": plugin}, "Delete plugin")
enabled_default = plugin.info.default == "enabled"
if plugin.enabled != plugin.loaded or plugin.updated
marker_title = "Change pending"
is_pending = true
else
marker_title = "Changed from default status (click to reset to #{plugin.info.default})"
is_pending = false
is_changed = plugin.enabled != enabled_default and plugin.owner == "builtin"
h("div.plugin", {key: plugin.name}, [
h("div.title", [
h("h3", [plugin.name, tag_version]),
h("div.description", [descr, tag_source, tag_delete]),
])
h("div.value.value-right",
h("div.checkbox", {onclick: @handleCheckboxChange, "data-plugin": plugin, classes: {checked: plugin.enabled}}, h("div.checkbox-skin"))
h("a.marker", {
href: "#Reset", title: marker_title,
onclick: @handleResetClick, "data-plugin": plugin,
classes: {visible: is_pending or is_changed, pending: is_pending}
}, "\u2022")
)
])
)
window.PluginList = PluginList

View file

@ -0,0 +1,71 @@
window.h = maquette.h
class UiPluginManager extends ZeroFrame
init: ->
@plugin_list_builtin = new PluginList()
@plugin_list_custom = new PluginList()
@plugins_changed = null
@need_restart = null
@
onOpenWebsocket: =>
@cmd("wrapperSetTitle", "Plugin manager - ZeroNet")
@cmd "serverInfo", {}, (server_info) =>
@server_info = server_info
@updatePlugins()
updatePlugins: (cb) =>
@cmd "pluginList", [], (res) =>
@plugins_changed = (item for item in res.plugins when item.enabled != item.loaded or item.updated)
plugins_builtin = (item for item in res.plugins when item.source == "builtin")
@plugin_list_builtin.plugins = plugins_builtin.sort (a, b) ->
return a.name.localeCompare(b.name)
plugins_custom = (item for item in res.plugins when item.source != "builtin")
@plugin_list_custom.plugins = plugins_custom.sort (a, b) ->
return a.name.localeCompare(b.name)
@projector.scheduleRender()
cb?()
createProjector: =>
@projector = maquette.createProjector()
@projector.replace($("#content"), @render)
@projector.replace($("#bottom-restart"), @renderBottomRestart)
render: =>
if not @plugin_list_builtin.plugins
return h("div.content")
h("div.content", [
h("div.section", [
if @plugin_list_custom.plugins?.length
[
h("h2", "Installed third-party plugins"),
@plugin_list_custom.render()
]
h("h2", "Built-in plugins")
@plugin_list_builtin.render()
])
])
handleRestartClick: =>
@restart_loading = true
setTimeout ( =>
Page.cmd("serverShutdown", {restart: true})
), 300
Page.projector.scheduleRender()
return false
renderBottomRestart: =>
h("div.bottom.bottom-restart", {classes: {visible: @plugins_changed?.length}}, h("div.bottom-content", [
h("div.title", "Some plugins status has been changed"),
h("a.button.button-submit.button-restart",
{href: "#Restart", classes: {loading: @restart_loading}, onclick: @handleRestartClick},
"Restart ZeroNet client"
)
]))
window.Page = new UiPluginManager()
window.Page.createProjector()

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,23 @@
class Class
trace: true
log: (args...) ->
return unless @trace
return if typeof console is 'undefined'
args.unshift("[#{@.constructor.name}]")
console.log(args...)
@
logStart: (name, args...) ->
return unless @trace
@logtimers or= {}
@logtimers[name] = +(new Date)
@log "#{name}", args..., "(started)" if args.length > 0
@
logEnd: (name, args...) ->
ms = +(new Date)-@logtimers[name]
@log "#{name}", args..., "(Done in #{ms}ms)"
@
window.Class = Class

View file

@ -0,0 +1,74 @@
# From: http://dev.bizo.com/2011/12/promises-in-javascriptcoffeescript.html
class Promise
@when: (tasks...) ->
num_uncompleted = tasks.length
args = new Array(num_uncompleted)
promise = new Promise()
for task, task_id in tasks
((task_id) ->
task.then(() ->
args[task_id] = Array.prototype.slice.call(arguments)
num_uncompleted--
promise.complete.apply(promise, args) if num_uncompleted == 0
)
)(task_id)
return promise
constructor: ->
@resolved = false
@end_promise = null
@result = null
@callbacks = []
resolve: ->
if @resolved
return false
@resolved = true
@data = arguments
if not arguments.length
@data = [true]
@result = @data[0]
for callback in @callbacks
back = callback.apply callback, @data
if @end_promise
@end_promise.resolve(back)
fail: ->
@resolve(false)
then: (callback) ->
if @resolved == true
callback.apply callback, @data
return
@callbacks.push callback
@end_promise = new Promise()
window.Promise = Promise
###
s = Date.now()
log = (text) ->
console.log Date.now()-s, Array.prototype.slice.call(arguments).join(", ")
log "Started"
cmd = (query) ->
p = new Promise()
setTimeout ( ->
p.resolve query+" Result"
), 100
return p
back = cmd("SELECT * FROM message").then (res) ->
log res
return "Return from query"
.then (res) ->
log "Back then", res
log "Query started", back
###

View file

@ -0,0 +1,8 @@
String::startsWith = (s) -> @[...s.length] is s
String::endsWith = (s) -> s is '' or @[-s.length..] is s
String::repeat = (count) -> new Array( count + 1 ).join(@)
window.isEmpty = (obj) ->
for key of obj
return false
return true

View file

@ -0,0 +1,770 @@
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports'], factory);
} else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') {
// CommonJS
factory(exports);
} else {
// Browser globals
factory(root.maquette = {});
}
}(this, function (exports) {
'use strict';
;
;
;
;
var NAMESPACE_W3 = 'http://www.w3.org/';
var NAMESPACE_SVG = NAMESPACE_W3 + '2000/svg';
var NAMESPACE_XLINK = NAMESPACE_W3 + '1999/xlink';
// Utilities
var emptyArray = [];
var extend = function (base, overrides) {
var result = {};
Object.keys(base).forEach(function (key) {
result[key] = base[key];
});
if (overrides) {
Object.keys(overrides).forEach(function (key) {
result[key] = overrides[key];
});
}
return result;
};
// Hyperscript helper functions
var same = function (vnode1, vnode2) {
if (vnode1.vnodeSelector !== vnode2.vnodeSelector) {
return false;
}
if (vnode1.properties && vnode2.properties) {
if (vnode1.properties.key !== vnode2.properties.key) {
return false;
}
return vnode1.properties.bind === vnode2.properties.bind;
}
return !vnode1.properties && !vnode2.properties;
};
var toTextVNode = function (data) {
return {
vnodeSelector: '',
properties: undefined,
children: undefined,
text: data.toString(),
domNode: null
};
};
var appendChildren = function (parentSelector, insertions, main) {
for (var i = 0; i < insertions.length; i++) {
var item = insertions[i];
if (Array.isArray(item)) {
appendChildren(parentSelector, item, main);
} else {
if (item !== null && item !== undefined) {
if (!item.hasOwnProperty('vnodeSelector')) {
item = toTextVNode(item);
}
main.push(item);
}
}
}
};
// Render helper functions
var missingTransition = function () {
throw new Error('Provide a transitions object to the projectionOptions to do animations');
};
var DEFAULT_PROJECTION_OPTIONS = {
namespace: undefined,
eventHandlerInterceptor: undefined,
styleApplyer: function (domNode, styleName, value) {
// Provides a hook to add vendor prefixes for browsers that still need it.
domNode.style[styleName] = value;
},
transitions: {
enter: missingTransition,
exit: missingTransition
}
};
var applyDefaultProjectionOptions = function (projectorOptions) {
return extend(DEFAULT_PROJECTION_OPTIONS, projectorOptions);
};
var checkStyleValue = function (styleValue) {
if (typeof styleValue !== 'string') {
throw new Error('Style values must be strings');
}
};
var setProperties = function (domNode, properties, projectionOptions) {
if (!properties) {
return;
}
var eventHandlerInterceptor = projectionOptions.eventHandlerInterceptor;
var propNames = Object.keys(properties);
var propCount = propNames.length;
for (var i = 0; i < propCount; i++) {
var propName = propNames[i];
/* tslint:disable:no-var-keyword: edge case */
var propValue = properties[propName];
/* tslint:enable:no-var-keyword */
if (propName === 'className') {
throw new Error('Property "className" is not supported, use "class".');
} else if (propName === 'class') {
if (domNode.className) {
// May happen if classes is specified before class
domNode.className += ' ' + propValue;
} else {
domNode.className = propValue;
}
} else if (propName === 'classes') {
// object with string keys and boolean values
var classNames = Object.keys(propValue);
var classNameCount = classNames.length;
for (var j = 0; j < classNameCount; j++) {
var className = classNames[j];
if (propValue[className]) {
domNode.classList.add(className);
}
}
} else if (propName === 'styles') {
// object with string keys and string (!) values
var styleNames = Object.keys(propValue);
var styleCount = styleNames.length;
for (var j = 0; j < styleCount; j++) {
var styleName = styleNames[j];
var styleValue = propValue[styleName];
if (styleValue) {
checkStyleValue(styleValue);
projectionOptions.styleApplyer(domNode, styleName, styleValue);
}
}
} else if (propName === 'key') {
continue;
} else if (propValue === null || propValue === undefined) {
continue;
} else {
var type = typeof propValue;
if (type === 'function') {
if (propName.lastIndexOf('on', 0) === 0) {
if (eventHandlerInterceptor) {
propValue = eventHandlerInterceptor(propName, propValue, domNode, properties); // intercept eventhandlers
}
if (propName === 'oninput') {
(function () {
// record the evt.target.value, because IE and Edge sometimes do a requestAnimationFrame between changing value and running oninput
var oldPropValue = propValue;
propValue = function (evt) {
evt.target['oninput-value'] = evt.target.value;
// may be HTMLTextAreaElement as well
oldPropValue.apply(this, [evt]);
};
}());
}
domNode[propName] = propValue;
}
} else if (type === 'string' && propName !== 'value' && propName !== 'innerHTML') {
if (projectionOptions.namespace === NAMESPACE_SVG && propName === 'href') {
domNode.setAttributeNS(NAMESPACE_XLINK, propName, propValue);
} else {
domNode.setAttribute(propName, propValue);
}
} else {
domNode[propName] = propValue;
}
}
}
};
var updateProperties = function (domNode, previousProperties, properties, projectionOptions) {
if (!properties) {
return;
}
var propertiesUpdated = false;
var propNames = Object.keys(properties);
var propCount = propNames.length;
for (var i = 0; i < propCount; i++) {
var propName = propNames[i];
// assuming that properties will be nullified instead of missing is by design
var propValue = properties[propName];
var previousValue = previousProperties[propName];
if (propName === 'class') {
if (previousValue !== propValue) {
throw new Error('"class" property may not be updated. Use the "classes" property for conditional css classes.');
}
} else if (propName === 'classes') {
var classList = domNode.classList;
var classNames = Object.keys(propValue);
var classNameCount = classNames.length;
for (var j = 0; j < classNameCount; j++) {
var className = classNames[j];
var on = !!propValue[className];
var previousOn = !!previousValue[className];
if (on === previousOn) {
continue;
}
propertiesUpdated = true;
if (on) {
classList.add(className);
} else {
classList.remove(className);
}
}
} else if (propName === 'styles') {
var styleNames = Object.keys(propValue);
var styleCount = styleNames.length;
for (var j = 0; j < styleCount; j++) {
var styleName = styleNames[j];
var newStyleValue = propValue[styleName];
var oldStyleValue = previousValue[styleName];
if (newStyleValue === oldStyleValue) {
continue;
}
propertiesUpdated = true;
if (newStyleValue) {
checkStyleValue(newStyleValue);
projectionOptions.styleApplyer(domNode, styleName, newStyleValue);
} else {
projectionOptions.styleApplyer(domNode, styleName, '');
}
}
} else {
if (!propValue && typeof previousValue === 'string') {
propValue = '';
}
if (propName === 'value') {
if (domNode[propName] !== propValue && domNode['oninput-value'] !== propValue) {
domNode[propName] = propValue;
// Reset the value, even if the virtual DOM did not change
domNode['oninput-value'] = undefined;
}
// else do not update the domNode, otherwise the cursor position would be changed
if (propValue !== previousValue) {
propertiesUpdated = true;
}
} else if (propValue !== previousValue) {
var type = typeof propValue;
if (type === 'function') {
throw new Error('Functions may not be updated on subsequent renders (property: ' + propName + '). Hint: declare event handler functions outside the render() function.');
}
if (type === 'string' && propName !== 'innerHTML') {
if (projectionOptions.namespace === NAMESPACE_SVG && propName === 'href') {
domNode.setAttributeNS(NAMESPACE_XLINK, propName, propValue);
} else {
domNode.setAttribute(propName, propValue);
}
} else {
if (domNode[propName] !== propValue) {
domNode[propName] = propValue;
}
}
propertiesUpdated = true;
}
}
}
return propertiesUpdated;
};
var findIndexOfChild = function (children, sameAs, start) {
if (sameAs.vnodeSelector !== '') {
// Never scan for text-nodes
for (var i = start; i < children.length; i++) {
if (same(children[i], sameAs)) {
return i;
}
}
}
return -1;
};
var nodeAdded = function (vNode, transitions) {
if (vNode.properties) {
var enterAnimation = vNode.properties.enterAnimation;
if (enterAnimation) {
if (typeof enterAnimation === 'function') {
enterAnimation(vNode.domNode, vNode.properties);
} else {
transitions.enter(vNode.domNode, vNode.properties, enterAnimation);
}
}
}
};
var nodeToRemove = function (vNode, transitions) {
var domNode = vNode.domNode;
if (vNode.properties) {
var exitAnimation = vNode.properties.exitAnimation;
if (exitAnimation) {
domNode.style.pointerEvents = 'none';
var removeDomNode = function () {
if (domNode.parentNode) {
domNode.parentNode.removeChild(domNode);
}
};
if (typeof exitAnimation === 'function') {
exitAnimation(domNode, removeDomNode, vNode.properties);
return;
} else {
transitions.exit(vNode.domNode, vNode.properties, exitAnimation, removeDomNode);
return;
}
}
}
if (domNode.parentNode) {
domNode.parentNode.removeChild(domNode);
}
};
var checkDistinguishable = function (childNodes, indexToCheck, parentVNode, operation) {
var childNode = childNodes[indexToCheck];
if (childNode.vnodeSelector === '') {
return; // Text nodes need not be distinguishable
}
var properties = childNode.properties;
var key = properties ? properties.key === undefined ? properties.bind : properties.key : undefined;
if (!key) {
for (var i = 0; i < childNodes.length; i++) {
if (i !== indexToCheck) {
var node = childNodes[i];
if (same(node, childNode)) {
if (operation === 'added') {
throw new Error(parentVNode.vnodeSelector + ' had a ' + childNode.vnodeSelector + ' child ' + 'added, but there is now more than one. You must add unique key properties to make them distinguishable.');
} else {
throw new Error(parentVNode.vnodeSelector + ' had a ' + childNode.vnodeSelector + ' child ' + 'removed, but there were more than one. You must add unique key properties to make them distinguishable.');
}
}
}
}
}
};
var createDom;
var updateDom;
var updateChildren = function (vnode, domNode, oldChildren, newChildren, projectionOptions) {
if (oldChildren === newChildren) {
return false;
}
oldChildren = oldChildren || emptyArray;
newChildren = newChildren || emptyArray;
var oldChildrenLength = oldChildren.length;
var newChildrenLength = newChildren.length;
var transitions = projectionOptions.transitions;
var oldIndex = 0;
var newIndex = 0;
var i;
var textUpdated = false;
while (newIndex < newChildrenLength) {
var oldChild = oldIndex < oldChildrenLength ? oldChildren[oldIndex] : undefined;
var newChild = newChildren[newIndex];
if (oldChild !== undefined && same(oldChild, newChild)) {
textUpdated = updateDom(oldChild, newChild, projectionOptions) || textUpdated;
oldIndex++;
} else {
var findOldIndex = findIndexOfChild(oldChildren, newChild, oldIndex + 1);
if (findOldIndex >= 0) {
// Remove preceding missing children
for (i = oldIndex; i < findOldIndex; i++) {
nodeToRemove(oldChildren[i], transitions);
checkDistinguishable(oldChildren, i, vnode, 'removed');
}
textUpdated = updateDom(oldChildren[findOldIndex], newChild, projectionOptions) || textUpdated;
oldIndex = findOldIndex + 1;
} else {
// New child
createDom(newChild, domNode, oldIndex < oldChildrenLength ? oldChildren[oldIndex].domNode : undefined, projectionOptions);
nodeAdded(newChild, transitions);
checkDistinguishable(newChildren, newIndex, vnode, 'added');
}
}
newIndex++;
}
if (oldChildrenLength > oldIndex) {
// Remove child fragments
for (i = oldIndex; i < oldChildrenLength; i++) {
nodeToRemove(oldChildren[i], transitions);
checkDistinguishable(oldChildren, i, vnode, 'removed');
}
}
return textUpdated;
};
var addChildren = function (domNode, children, projectionOptions) {
if (!children) {
return;
}
for (var i = 0; i < children.length; i++) {
createDom(children[i], domNode, undefined, projectionOptions);
}
};
var initPropertiesAndChildren = function (domNode, vnode, projectionOptions) {
addChildren(domNode, vnode.children, projectionOptions);
// children before properties, needed for value property of <select>.
if (vnode.text) {
domNode.textContent = vnode.text;
}
setProperties(domNode, vnode.properties, projectionOptions);
if (vnode.properties && vnode.properties.afterCreate) {
vnode.properties.afterCreate(domNode, projectionOptions, vnode.vnodeSelector, vnode.properties, vnode.children);
}
};
createDom = function (vnode, parentNode, insertBefore, projectionOptions) {
var domNode, i, c, start = 0, type, found;
var vnodeSelector = vnode.vnodeSelector;
if (vnodeSelector === '') {
domNode = vnode.domNode = document.createTextNode(vnode.text);
if (insertBefore !== undefined) {
parentNode.insertBefore(domNode, insertBefore);
} else {
parentNode.appendChild(domNode);
}
} else {
for (i = 0; i <= vnodeSelector.length; ++i) {
c = vnodeSelector.charAt(i);
if (i === vnodeSelector.length || c === '.' || c === '#') {
type = vnodeSelector.charAt(start - 1);
found = vnodeSelector.slice(start, i);
if (type === '.') {
domNode.classList.add(found);
} else if (type === '#') {
domNode.id = found;
} else {
if (found === 'svg') {
projectionOptions = extend(projectionOptions, { namespace: NAMESPACE_SVG });
}
if (projectionOptions.namespace !== undefined) {
domNode = vnode.domNode = document.createElementNS(projectionOptions.namespace, found);
} else {
domNode = vnode.domNode = document.createElement(found);
}
if (insertBefore !== undefined) {
parentNode.insertBefore(domNode, insertBefore);
} else {
parentNode.appendChild(domNode);
}
}
start = i + 1;
}
}
initPropertiesAndChildren(domNode, vnode, projectionOptions);
}
};
updateDom = function (previous, vnode, projectionOptions) {
var domNode = previous.domNode;
var textUpdated = false;
if (previous === vnode) {
return false; // By contract, VNode objects may not be modified anymore after passing them to maquette
}
var updated = false;
if (vnode.vnodeSelector === '') {
if (vnode.text !== previous.text) {
var newVNode = document.createTextNode(vnode.text);
domNode.parentNode.replaceChild(newVNode, domNode);
vnode.domNode = newVNode;
textUpdated = true;
return textUpdated;
}
} else {
if (vnode.vnodeSelector.lastIndexOf('svg', 0) === 0) {
projectionOptions = extend(projectionOptions, { namespace: NAMESPACE_SVG });
}
if (previous.text !== vnode.text) {
updated = true;
if (vnode.text === undefined) {
domNode.removeChild(domNode.firstChild); // the only textnode presumably
} else {
domNode.textContent = vnode.text;
}
}
updated = updateChildren(vnode, domNode, previous.children, vnode.children, projectionOptions) || updated;
updated = updateProperties(domNode, previous.properties, vnode.properties, projectionOptions) || updated;
if (vnode.properties && vnode.properties.afterUpdate) {
vnode.properties.afterUpdate(domNode, projectionOptions, vnode.vnodeSelector, vnode.properties, vnode.children);
}
}
if (updated && vnode.properties && vnode.properties.updateAnimation) {
vnode.properties.updateAnimation(domNode, vnode.properties, previous.properties);
}
vnode.domNode = previous.domNode;
return textUpdated;
};
var createProjection = function (vnode, projectionOptions) {
return {
update: function (updatedVnode) {
if (vnode.vnodeSelector !== updatedVnode.vnodeSelector) {
throw new Error('The selector for the root VNode may not be changed. (consider using dom.merge and add one extra level to the virtual DOM)');
}
updateDom(vnode, updatedVnode, projectionOptions);
vnode = updatedVnode;
},
domNode: vnode.domNode
};
};
;
// The other two parameters are not added here, because the Typescript compiler creates surrogate code for desctructuring 'children'.
exports.h = function (selector) {
var properties = arguments[1];
if (typeof selector !== 'string') {
throw new Error();
}
var childIndex = 1;
if (properties && !properties.hasOwnProperty('vnodeSelector') && !Array.isArray(properties) && typeof properties === 'object') {
childIndex = 2;
} else {
// Optional properties argument was omitted
properties = undefined;
}
var text = undefined;
var children = undefined;
var argsLength = arguments.length;
// Recognize a common special case where there is only a single text node
if (argsLength === childIndex + 1) {
var onlyChild = arguments[childIndex];
if (typeof onlyChild === 'string') {
text = onlyChild;
} else if (onlyChild !== undefined && onlyChild.length === 1 && typeof onlyChild[0] === 'string') {
text = onlyChild[0];
}
}
if (text === undefined) {
children = [];
for (; childIndex < arguments.length; childIndex++) {
var child = arguments[childIndex];
if (child === null || child === undefined) {
continue;
} else if (Array.isArray(child)) {
appendChildren(selector, child, children);
} else if (child.hasOwnProperty('vnodeSelector')) {
children.push(child);
} else {
children.push(toTextVNode(child));
}
}
}
return {
vnodeSelector: selector,
properties: properties,
children: children,
text: text === '' ? undefined : text,
domNode: null
};
};
/**
* Contains simple low-level utility functions to manipulate the real DOM.
*/
exports.dom = {
/**
* Creates a real DOM tree from `vnode`. The [[Projection]] object returned will contain the resulting DOM Node in
* its [[Projection.domNode|domNode]] property.
* This is a low-level method. Users wil typically use a [[Projector]] instead.
* @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]]
* objects may only be rendered once.
* @param projectionOptions - Options to be used to create and update the projection.
* @returns The [[Projection]] which also contains the DOM Node that was created.
*/
create: function (vnode, projectionOptions) {
projectionOptions = applyDefaultProjectionOptions(projectionOptions);
createDom(vnode, document.createElement('div'), undefined, projectionOptions);
return createProjection(vnode, projectionOptions);
},
/**
* Appends a new childnode to the DOM which is generated from a [[VNode]].
* This is a low-level method. Users wil typically use a [[Projector]] instead.
* @param parentNode - The parent node for the new childNode.
* @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]]
* objects may only be rendered once.
* @param projectionOptions - Options to be used to create and update the [[Projection]].
* @returns The [[Projection]] that was created.
*/
append: function (parentNode, vnode, projectionOptions) {
projectionOptions = applyDefaultProjectionOptions(projectionOptions);
createDom(vnode, parentNode, undefined, projectionOptions);
return createProjection(vnode, projectionOptions);
},
/**
* Inserts a new DOM node which is generated from a [[VNode]].
* This is a low-level method. Users wil typically use a [[Projector]] instead.
* @param beforeNode - The node that the DOM Node is inserted before.
* @param vnode - The root of the virtual DOM tree that was created using the [[h]] function.
* NOTE: [[VNode]] objects may only be rendered once.
* @param projectionOptions - Options to be used to create and update the projection, see [[createProjector]].
* @returns The [[Projection]] that was created.
*/
insertBefore: function (beforeNode, vnode, projectionOptions) {
projectionOptions = applyDefaultProjectionOptions(projectionOptions);
createDom(vnode, beforeNode.parentNode, beforeNode, projectionOptions);
return createProjection(vnode, projectionOptions);
},
/**
* Merges a new DOM node which is generated from a [[VNode]] with an existing DOM Node.
* This means that the virtual DOM and the real DOM will have one overlapping element.
* Therefore the selector for the root [[VNode]] will be ignored, but its properties and children will be applied to the Element provided.
* This is a low-level method. Users wil typically use a [[Projector]] instead.
* @param domNode - The existing element to adopt as the root of the new virtual DOM. Existing attributes and childnodes are preserved.
* @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]] objects
* may only be rendered once.
* @param projectionOptions - Options to be used to create and update the projection, see [[createProjector]].
* @returns The [[Projection]] that was created.
*/
merge: function (element, vnode, projectionOptions) {
projectionOptions = applyDefaultProjectionOptions(projectionOptions);
vnode.domNode = element;
initPropertiesAndChildren(element, vnode, projectionOptions);
return createProjection(vnode, projectionOptions);
}
};
/**
* Creates a [[CalculationCache]] object, useful for caching [[VNode]] trees.
* In practice, caching of [[VNode]] trees is not needed, because achieving 60 frames per second is almost never a problem.
* For more information, see [[CalculationCache]].
*
* @param <Result> The type of the value that is cached.
*/
exports.createCache = function () {
var cachedInputs = undefined;
var cachedOutcome = undefined;
var result = {
invalidate: function () {
cachedOutcome = undefined;
cachedInputs = undefined;
},
result: function (inputs, calculation) {
if (cachedInputs) {
for (var i = 0; i < inputs.length; i++) {
if (cachedInputs[i] !== inputs[i]) {
cachedOutcome = undefined;
}
}
}
if (!cachedOutcome) {
cachedOutcome = calculation();
cachedInputs = inputs;
}
return cachedOutcome;
}
};
return result;
};
/**
* Creates a {@link Mapping} instance that keeps an array of result objects synchronized with an array of source objects.
* See {@link http://maquettejs.org/docs/arrays.html|Working with arrays}.
*
* @param <Source> The type of source items. A database-record for instance.
* @param <Target> The type of target items. A [[Component]] for instance.
* @param getSourceKey `function(source)` that must return a key to identify each source object. The result must either be a string or a number.
* @param createResult `function(source, index)` that must create a new result object from a given source. This function is identical
* to the `callback` argument in `Array.map(callback)`.
* @param updateResult `function(source, target, index)` that updates a result to an updated source.
*/
exports.createMapping = function (getSourceKey, createResult, updateResult) {
var keys = [];
var results = [];
return {
results: results,
map: function (newSources) {
var newKeys = newSources.map(getSourceKey);
var oldTargets = results.slice();
var oldIndex = 0;
for (var i = 0; i < newSources.length; i++) {
var source = newSources[i];
var sourceKey = newKeys[i];
if (sourceKey === keys[oldIndex]) {
results[i] = oldTargets[oldIndex];
updateResult(source, oldTargets[oldIndex], i);
oldIndex++;
} else {
var found = false;
for (var j = 1; j < keys.length; j++) {
var searchIndex = (oldIndex + j) % keys.length;
if (keys[searchIndex] === sourceKey) {
results[i] = oldTargets[searchIndex];
updateResult(newSources[i], oldTargets[searchIndex], i);
oldIndex = searchIndex + 1;
found = true;
break;
}
}
if (!found) {
results[i] = createResult(source, i);
}
}
}
results.length = newSources.length;
keys = newKeys;
}
};
};
/**
* Creates a [[Projector]] instance using the provided projectionOptions.
*
* For more information, see [[Projector]].
*
* @param projectionOptions Options that influence how the DOM is rendered and updated.
*/
exports.createProjector = function (projectorOptions) {
var projector;
var projectionOptions = applyDefaultProjectionOptions(projectorOptions);
projectionOptions.eventHandlerInterceptor = function (propertyName, eventHandler, domNode, properties) {
return function () {
// intercept function calls (event handlers) to do a render afterwards.
projector.scheduleRender();
return eventHandler.apply(properties.bind || this, arguments);
};
};
var renderCompleted = true;
var scheduled;
var stopped = false;
var projections = [];
var renderFunctions = [];
// matches the projections array
var doRender = function () {
scheduled = undefined;
if (!renderCompleted) {
return; // The last render threw an error, it should be logged in the browser console.
}
renderCompleted = false;
for (var i = 0; i < projections.length; i++) {
var updatedVnode = renderFunctions[i]();
projections[i].update(updatedVnode);
}
renderCompleted = true;
};
projector = {
scheduleRender: function () {
if (!scheduled && !stopped) {
scheduled = requestAnimationFrame(doRender);
}
},
stop: function () {
if (scheduled) {
cancelAnimationFrame(scheduled);
scheduled = undefined;
}
stopped = true;
},
resume: function () {
stopped = false;
renderCompleted = true;
projector.scheduleRender();
},
append: function (parentNode, renderMaquetteFunction) {
projections.push(exports.dom.append(parentNode, renderMaquetteFunction(), projectionOptions));
renderFunctions.push(renderMaquetteFunction);
},
insertBefore: function (beforeNode, renderMaquetteFunction) {
projections.push(exports.dom.insertBefore(beforeNode, renderMaquetteFunction(), projectionOptions));
renderFunctions.push(renderMaquetteFunction);
},
merge: function (domNode, renderMaquetteFunction) {
projections.push(exports.dom.merge(domNode, renderMaquetteFunction(), projectionOptions));
renderFunctions.push(renderMaquetteFunction);
},
replace: function (domNode, renderMaquetteFunction) {
var vnode = renderMaquetteFunction();
createDom(vnode, domNode.parentNode, domNode, projectionOptions);
domNode.parentNode.removeChild(domNode);
projections.push(createProjection(vnode, projectionOptions));
renderFunctions.push(renderMaquetteFunction);
},
detach: function (renderMaquetteFunction) {
for (var i = 0; i < renderFunctions.length; i++) {
if (renderFunctions[i] === renderMaquetteFunction) {
renderFunctions.splice(i, 1);
return projections.splice(i, 1)[0];
}
}
throw new Error('renderMaquetteFunction was not found');
}
};
return projector;
};
}));

View file

@ -0,0 +1,138 @@
class Animation
slideDown: (elem, props) ->
if elem.offsetTop > 2000
return
h = elem.offsetHeight
cstyle = window.getComputedStyle(elem)
margin_top = cstyle.marginTop
margin_bottom = cstyle.marginBottom
padding_top = cstyle.paddingTop
padding_bottom = cstyle.paddingBottom
transition = cstyle.transition
elem.style.boxSizing = "border-box"
elem.style.overflow = "hidden"
elem.style.transform = "scale(0.6)"
elem.style.opacity = "0"
elem.style.height = "0px"
elem.style.marginTop = "0px"
elem.style.marginBottom = "0px"
elem.style.paddingTop = "0px"
elem.style.paddingBottom = "0px"
elem.style.transition = "none"
setTimeout (->
elem.className += " animate-inout"
elem.style.height = h+"px"
elem.style.transform = "scale(1)"
elem.style.opacity = "1"
elem.style.marginTop = margin_top
elem.style.marginBottom = margin_bottom
elem.style.paddingTop = padding_top
elem.style.paddingBottom = padding_bottom
), 1
elem.addEventListener "transitionend", ->
elem.classList.remove("animate-inout")
elem.style.transition = elem.style.transform = elem.style.opacity = elem.style.height = null
elem.style.boxSizing = elem.style.marginTop = elem.style.marginBottom = null
elem.style.paddingTop = elem.style.paddingBottom = elem.style.overflow = null
elem.removeEventListener "transitionend", arguments.callee, false
slideUp: (elem, remove_func, props) ->
if elem.offsetTop > 1000
return remove_func()
elem.className += " animate-back"
elem.style.boxSizing = "border-box"
elem.style.height = elem.offsetHeight+"px"
elem.style.overflow = "hidden"
elem.style.transform = "scale(1)"
elem.style.opacity = "1"
elem.style.pointerEvents = "none"
setTimeout (->
elem.style.height = "0px"
elem.style.marginTop = "0px"
elem.style.marginBottom = "0px"
elem.style.paddingTop = "0px"
elem.style.paddingBottom = "0px"
elem.style.transform = "scale(0.8)"
elem.style.borderTopWidth = "0px"
elem.style.borderBottomWidth = "0px"
elem.style.opacity = "0"
), 1
elem.addEventListener "transitionend", (e) ->
if e.propertyName == "opacity" or e.elapsedTime >= 0.6
elem.removeEventListener "transitionend", arguments.callee, false
remove_func()
slideUpInout: (elem, remove_func, props) ->
elem.className += " animate-inout"
elem.style.boxSizing = "border-box"
elem.style.height = elem.offsetHeight+"px"
elem.style.overflow = "hidden"
elem.style.transform = "scale(1)"
elem.style.opacity = "1"
elem.style.pointerEvents = "none"
setTimeout (->
elem.style.height = "0px"
elem.style.marginTop = "0px"
elem.style.marginBottom = "0px"
elem.style.paddingTop = "0px"
elem.style.paddingBottom = "0px"
elem.style.transform = "scale(0.8)"
elem.style.borderTopWidth = "0px"
elem.style.borderBottomWidth = "0px"
elem.style.opacity = "0"
), 1
elem.addEventListener "transitionend", (e) ->
if e.propertyName == "opacity" or e.elapsedTime >= 0.6
elem.removeEventListener "transitionend", arguments.callee, false
remove_func()
showRight: (elem, props) ->
elem.className += " animate"
elem.style.opacity = 0
elem.style.transform = "TranslateX(-20px) Scale(1.01)"
setTimeout (->
elem.style.opacity = 1
elem.style.transform = "TranslateX(0px) Scale(1)"
), 1
elem.addEventListener "transitionend", ->
elem.classList.remove("animate")
elem.style.transform = elem.style.opacity = null
show: (elem, props) ->
delay = arguments[arguments.length-2]?.delay*1000 or 1
elem.style.opacity = 0
setTimeout (->
elem.className += " animate"
), 1
setTimeout (->
elem.style.opacity = 1
), delay
elem.addEventListener "transitionend", ->
elem.classList.remove("animate")
elem.style.opacity = null
elem.removeEventListener "transitionend", arguments.callee, false
hide: (elem, remove_func, props) ->
delay = arguments[arguments.length-2]?.delay*1000 or 1
elem.className += " animate"
setTimeout (->
elem.style.opacity = 0
), delay
elem.addEventListener "transitionend", (e) ->
if e.propertyName == "opacity"
remove_func()
addVisibleClass: (elem, props) ->
setTimeout ->
elem.classList.add("visible")
window.Animation = new Animation()

View file

@ -0,0 +1,3 @@
window.$ = (selector) ->
if selector.startsWith("#")
return document.getElementById(selector.replace("#", ""))

View file

@ -0,0 +1,85 @@
class ZeroFrame extends Class
constructor: (url) ->
@url = url
@waiting_cb = {}
@wrapper_nonce = document.location.href.replace(/.*wrapper_nonce=([A-Za-z0-9]+).*/, "$1")
@connect()
@next_message_id = 1
@history_state = {}
@init()
init: ->
@
connect: ->
@target = window.parent
window.addEventListener("message", @onMessage, false)
@cmd("innerReady")
# Save scrollTop
window.addEventListener "beforeunload", (e) =>
@log "save scrollTop", window.pageYOffset
@history_state["scrollTop"] = window.pageYOffset
@cmd "wrapperReplaceState", [@history_state, null]
# Restore scrollTop
@cmd "wrapperGetState", [], (state) =>
@history_state = state if state?
@log "restore scrollTop", state, window.pageYOffset
if window.pageYOffset == 0 and state
window.scroll(window.pageXOffset, state.scrollTop)
onMessage: (e) =>
message = e.data
cmd = message.cmd
if cmd == "response"
if @waiting_cb[message.to]?
@waiting_cb[message.to](message.result)
else
@log "Websocket callback not found:", message
else if cmd == "wrapperReady" # Wrapper inited later
@cmd("innerReady")
else if cmd == "ping"
@response message.id, "pong"
else if cmd == "wrapperOpenedWebsocket"
@onOpenWebsocket()
else if cmd == "wrapperClosedWebsocket"
@onCloseWebsocket()
else
@onRequest cmd, message.params
onRequest: (cmd, message) =>
@log "Unknown request", message
response: (to, result) ->
@send {"cmd": "response", "to": to, "result": result}
cmd: (cmd, params={}, cb=null) ->
@send {"cmd": cmd, "params": params}, cb
send: (message, cb=null) ->
message.wrapper_nonce = @wrapper_nonce
message.id = @next_message_id
@next_message_id += 1
@target.postMessage(message, "*")
if cb
@waiting_cb[message.id] = cb
onOpenWebsocket: =>
@log "Websocket open"
onCloseWebsocket: =>
@log "Websocket close"
window.ZeroFrame = ZeroFrame

View file

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<title>Settings - ZeroNet</title>
<meta charset="utf-8" />
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<link rel="stylesheet" href="css/all.css?rev={rev}" />
</head>
<h1>ZeroNet plugin manager</h1>
<div class="content" id="content"></div>
<div class="bottom" id="bottom-restart"></div>
<script type="text/javascript" src="js/all.js?rev={rev}&lang={lang}"></script>
</body>
</html>

View file

@ -22,6 +22,7 @@ rpcpassword=your-password
rpcport=8336
server=1
txindex=1
valueencoding=utf8
```
Don't forget to change the `rpcuser` value and `rpcpassword` value!

View file

@ -13,7 +13,7 @@ log = logging.getLogger("ZeronamePlugin")
@PluginManager.registerTo("SiteManager")
class SiteManagerPlugin(object):
site_zeroname = None
db_domains = None
db_domains = {}
db_domains_modified = None
def load(self, *args, **kwargs):
@ -21,21 +21,13 @@ class SiteManagerPlugin(object):
if not self.get(config.bit_resolver):
self.need(config.bit_resolver) # Need ZeroName site
# Checks if it's a valid address
def isAddress(self, address):
return self.isBitDomain(address) or super(SiteManagerPlugin, self).isAddress(address)
# Return: True if the address is domain
def isDomain(self, address):
return self.isBitDomain(address) or super(SiteManagerPlugin, self).isDomain(address)
# Return: True if the address is .bit domain
def isBitDomain(self, address):
return re.match(r"(.*?)([A-Za-z0-9_-]+\.bit)$", address)
# Resolve domain
# Return: The address or None
def resolveDomain(self, domain):
def resolveBitDomain(self, domain):
domain = domain.lower()
if not self.site_zeroname:
self.site_zeroname = self.need(config.bit_resolver)
@ -44,7 +36,11 @@ class SiteManagerPlugin(object):
if not self.db_domains or self.db_domains_modified != site_zeroname_modified:
self.site_zeroname.needFile("data/names.json", priority=10)
s = time.time()
try:
self.db_domains = self.site_zeroname.storage.loadJson("data/names.json")
except Exception as err:
log.error("Error loading names.json: %s" % err)
log.debug(
"Domain db with %s entries loaded in %.3fs (modification: %s -> %s)" %
(len(self.db_domains), time.time() - s, self.db_domains_modified, site_zeroname_modified)
@ -52,33 +48,22 @@ class SiteManagerPlugin(object):
self.db_domains_modified = site_zeroname_modified
return self.db_domains.get(domain)
# Return or create site and start download site files
# Return: Site or None if dns resolve failed
def need(self, address, *args, **kwargs):
if self.isBitDomain(address): # Its looks like a domain
address_resolved = self.resolveDomain(address)
if address_resolved:
address = address_resolved
else:
return None
# Turn domain into address
def resolveDomain(self, domain):
return self.resolveBitDomain(domain) or super(SiteManagerPlugin, self).resolveDomain(domain)
return super(SiteManagerPlugin, self).need(address, *args, **kwargs)
# Return: True if the address is domain
def isDomain(self, address):
return self.isBitDomain(address) or super(SiteManagerPlugin, self).isDomain(address)
# Return: Site object or None if not found
def get(self, address):
if not self.loaded: # Not loaded yet
self.load()
if self.isBitDomain(address): # Its looks like a domain
address_resolved = self.resolveDomain(address)
if address_resolved: # Domain found
site = self.sites.get(address_resolved)
if site:
site_domain = site.settings.get("domain")
if site_domain != address:
site.settings["domain"] = address
else: # Domain not found
site = self.sites.get(address)
else: # Access by site address
site = super(SiteManagerPlugin, self).get(address)
return site
@PluginManager.registerTo("ConfigPlugin")
class ConfigPlugin(object):
def createArguments(self):
group = self.parser.add_argument_group("Zeroname plugin")
group.add_argument(
"--bit_resolver", help="ZeroNet site to resolve .bit domains",
default="1Name2NXVi1RDPDgf5617UoW7xA6YrhM9F", metavar="address"
)
return super(ConfigPlugin, self).createArguments()

View file

@ -1,30 +0,0 @@
import re
from Plugin import PluginManager
@PluginManager.registerTo("UiRequest")
class UiRequestPlugin(object):
def __init__(self, *args, **kwargs):
from Site import SiteManager
self.site_manager = SiteManager.site_manager
super(UiRequestPlugin, self).__init__(*args, **kwargs)
# Media request
def actionSiteMedia(self, path, **kwargs):
match = re.match(r"/media/(?P<address>[A-Za-z0-9-]+\.[A-Za-z0-9\.-]+)(?P<inner_path>/.*|$)", path)
if match: # Its a valid domain, resolve first
domain = match.group("address")
address = self.site_manager.resolveDomain(domain)
if address:
path = "/media/" + address + match.group("inner_path")
return super(UiRequestPlugin, self).actionSiteMedia(path, **kwargs) # Get the wrapper frame output
@PluginManager.registerTo("ConfigPlugin")
class ConfigPlugin(object):
def createArguments(self):
group = self.parser.add_argument_group("Zeroname plugin")
group.add_argument('--bit_resolver', help='ZeroNet site to resolve .bit domains', default="1Name2NXVi1RDPDgf5617UoW7xA6YrhM9F", metavar="address")
return super(ConfigPlugin, self).createArguments()

View file

@ -1,2 +1 @@
from . import UiRequestPlugin
from . import SiteManagerPlugin

View file

@ -79,8 +79,8 @@ class BootstrapperDb(Db.Db):
def getHashId(self, hash):
if hash not in self.hash_ids:
self.log.debug("New hash: %s" % repr(hash))
self.execute("INSERT OR IGNORE INTO hash ?", {"hash": hash})
self.hash_ids[hash] = self.cur.cursor.lastrowid
res = self.execute("INSERT OR IGNORE INTO hash ?", {"hash": hash})
self.hash_ids[hash] = res.lastrowid
return self.hash_ids[hash]
def peerAnnounce(self, ip_type, address, port=None, hashes=[], onion_signed=False, delete_missing_hashes=False):
@ -100,8 +100,8 @@ class BootstrapperDb(Db.Db):
self.log.debug("New peer: %s signed: %s" % (address, onion_signed))
if ip_type == "onion" and not onion_signed:
return len(hashes)
self.execute("INSERT INTO peer ?", {"type": ip_type, "address": address, "port": port, "date_announced": now})
peer_id = self.cur.cursor.lastrowid
res = self.execute("INSERT INTO peer ?", {"type": ip_type, "address": address, "port": port, "date_announced": now})
peer_id = res.lastrowid
# Check user's hashes
res = self.execute("SELECT * FROM peer_to_hash WHERE ?", {"peer_id": peer_id})

View file

@ -150,7 +150,7 @@ class UiRequestPlugin(object):
).fetchall()
yield "<br>%s (added: %s, peers: %s)<br>" % (
str(hash_row["hash"]).encode("hex"), hash_row["date_added"], len(peer_rows)
str(hash_row["hash"]).encode().hex(), hash_row["date_added"], len(peer_rows)
)
for peer_row in peer_rows:
yield " - {ip4: <30} {onion: <30} added: {date_added}, announced: {date_announced}<br>".format(**dict(peer_row))
yield " - {type} {address}:{port} added: {date_added}, announced: {date_announced}<br>".format(**dict(peer_row))

View file

@ -2,4 +2,5 @@
python_files = Test*.py
addopts = -rsxX -v --durations=6
markers =
slow: mark a tests as slow.
webtest: mark a test as a webtest.

View file

@ -0,0 +1,5 @@
{
"name": "Bootstrapper",
"description": "Add BitTorrent tracker server like features to your ZeroNet client.",
"default": "disabled"
}

Some files were not shown because too many files have changed in this diff Show more