This commit is contained in:
Pramukesh 2024-11-23 10:56:45 +00:00 committed by GitHub
commit 29f7ca2e35
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
178 changed files with 3999 additions and 9017 deletions

5
.dockerignore Normal file
View file

@ -0,0 +1,5 @@
venv
docker
data
__pycahce__
log

4
.github/FUNDING.yml vendored
View file

@ -1 +1,3 @@
custom: https://zeronet.io/docs/help_zeronet/donate/ liberapay: caryoscelus
ko_fi: caryoscelus
custom: https://caryoscelus.github.io/donate/

View file

@ -2,19 +2,9 @@
name: Feature request name: Feature request
about: Suggest an idea for ZeroNet about: Suggest an idea for ZeroNet
title: '' title: ''
labels: '' labels: 'enhancement'
assignees: '' assignees: ''
--- ---
**Is your feature request related to a problem? Please describe.** *we have to rigid structure for feature requests right now, but please try to include important details on the matter*
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

66
.github/workflows/codeql-analysis.yml vendored Normal file
View file

@ -0,0 +1,66 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
workflow_dispatch:
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript', 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
# - name: Autobuild
# uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
- run: |
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

View file

@ -1,6 +1,7 @@
name: tests name: tests
on: [push, pull_request] on:
workflow_dispatch:
jobs: jobs:
test: test:
@ -9,7 +10,7 @@ jobs:
strategy: strategy:
max-parallel: 16 max-parallel: 16
matrix: matrix:
python-version: [3.5, 3.6, 3.7, 3.8, 3.9] python-version: [3.6, 3.7, 3.8, 3.9, 3.10]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2

15
.gitignore vendored
View file

@ -7,6 +7,7 @@ __pycache__/
# Hidden files # Hidden files
.* .*
!.dockerignore
!/.github !/.github
!/.gitignore !/.gitignore
!/.travis.yml !/.travis.yml
@ -14,13 +15,16 @@ __pycache__/
# Temporary files # Temporary files
*.bak *.bak
*~
# Data dir # Data dir
data/* data/*
docker/data/
*.db *.db
# Virtualenv # Virtualenv
env/* env/*
venv/*
# Tor data # Tor data
tools/tor/data tools/tor/data
@ -33,3 +37,14 @@ zeronet.conf
# ZeroNet log files # ZeroNet log files
log/* log/*
# Enabled plugins that disabled by default
plugins/Bootstrapper
plugins/DonationMessage
plugins/Multiuser
plugins/NoNewSites
plugins/StemPort
plugins/UiPassword
# Build files
src/Build.py

View file

@ -1,10 +1,7 @@
language: python language: python
python: python:
- 3.4 - 3.9
- 3.5 - 3.10
- 3.6
- 3.7
- 3.8
services: services:
- docker - docker
cache: pip cache: pip
@ -40,8 +37,3 @@ after_failure:
after_success: after_success:
- codecov - codecov
- coveralls --rcfile=src/Test/coverage.ini - coveralls --rcfile=src/Test/coverage.ini
notifications:
email:
recipients:
hello@zeronet.io
on_success: change

View file

@ -1,6 +1,208 @@
### ZeroNet 0.7.2 (2020-09-?) Rev4206? ### zeronet-conservancy 0.7.10+
- disable site-plugins installed for security reasons (@caryoscelus)
- fix downloading geoip db (@caryoscelus)
- python <3.8 is officially unsupported
- SafeRe improvements by @geekless
- remove and don't update muted files (@caryoscelus)
- option to disable port checking (@caryoscelus)
- new install startup improvements (@caryoscelus)
- fix blank site with UiPassword under certain conditions (reported & sponsored by @bitcoren) (@caryoscelus)
- fix chromium compatibility (@caryoscelus)
- better fix of local sites leak (@caryoscelus)
- ipython-based repl via --repl for debug/interactive development (@caryoscelus)
- optional blocking of compromised id certificates for spam protection (@caryoscelus)
- changes in directory structure (split data and config, use user directories by default)
- use version information from git if available
- different build types (portable vs package)
- various improvements
### zeronet-conservancy 0.7.10 (2023-07-26) (18d35d3bed4f0683e99)
prepared by @caryoscelus
- update merkletools dependency to avoid legacy pysha3 (@caryoscelus)
- fix ReDoS in file editor (UiFileManager plugin) due to outdated codemirror (@caryoscelus)
- more anonymous UPnP (thanks to 0netdwf for reporting)
- remove old zeroname plugins (.bit deprecation)
- sitePublish --recursive option
- windows build instruction improvement
- importBundle command line action
- use Syncronite tracker site by default
- fix leak of local 0net sites to clearnet sites (originally reported by @egosown/beardog)
- various improvements
### zeronet-conservancy 0.7.9 (2023-07-02) (f966a4203fe33bd9f35)
maintainers: @caryoscelus -> none
- update README (build/dev instructions; thanks to @fgaz)
- better debugging of update non-propagation
- sec update of msgpck dependency (@chncaption)
- deprecate python-3.6 as it apparently is no longer used (by active users)
- improvement in imports and naming (@caryoscelus)
- siteSign accepts absolute paths as well as paths relative to working directory (@caryoscelus)
- updated trackers from Syncronite by @Styromaniac
- no longer officially maintained
### zeronet-conservancy 0.7.8.1 (2022-11-28) (0054eca9df0c9c8c2f4a78)
maintainers: @caryoscelus
- fix favourite/unfavourite in sidebar
- fix tracker connection regression introduced in dc804b9d5f3a2a9f1fffa1b97d82e0e04c44508b (thanks to @bitcoren)
- GiveUpGitHub notice
- update README
- new, more compact boot logo in console (more suitable for small screens)
### zeronet-conservancy 0.7.8 (2022-11-23) (110307a4198cb13cc907ae)
maintainers: @caryoscelus
- use archived version of .bit domain registry to avoid malicious rewrites
- readdress .bit domains as part of their deprecation
- remove potential vulnerability via setuptools (@ajesse11x)
- improve copying peers from sidebar
- reduce fingerprinting information available to unprivileged sites
- improve starting script
- fix default ssl version to be secure
- disable geoip-related ip address leak when in tor-only mode
- windows os build/running instruction (WIP)
- better command line parsing
- ArchLinux AUR package
- update android instruction (thanks oseido for reporting)
- better browser launch handling
- ability to add/remove from favourites from sidebar
- NoNewSites plugin
- show help message even when startup fails
- fix plugin options handling regression
- multiple code improvements
### zeronet-conservancy 0.7.7 (2022-07-27) (f40dbfeb2163b9902495ba)
maintainers: @caryoscelus, @FraYoshi, @prtngn, @d47081 (ex @d4708)
- return UPnP using secure xml library (@caryoscelus)
- xmr donations in sidebar fixed (@caryoscelus)
- add libffi-dev dependency (@d47081)
- add more deb dependencies (@BlueRook)
- add pyaes as external dependency (@caryoscelus)
- remove built-in pyaes (thanks to @emdee-net)
- fix error messages in sidebar (@caryoscelus)
- reduce fingerprinting of site owner (@caryoscelus)
- minor code improvements and reduce fingerprinting from zeronet-enhanced (by @geekless, adopted by @caryoscelus)
- improve and speed up content.json loading (@caryoscelus)
- show `--help` even if data directory is inaccessible (@caryoscelus)
- ask for optional user mute reason (@caryoscelus)
- multiple code improvements
### zeronet-conservancy 0.7.6 (2022-06-10) (864b7feb797d12077)
maintainers: @caryoscelus, @d4708, @FraYoshi, @prtngn
- more trackers from Syncronite by default
- introduce multiple donations methods (@caryoscelus)
- easier termux startup script (@unmanbearpig)
- Brazilian Portuguese readme translation (@iFallenHunt)
- reduce fingerprinting information (@caryoscelus)
- only enable site donations when activated (@caryoscelus)
- new docker files (@prtngn)
- updated Russian readme (@Programmator9000)
- multiple improvements in sidebar button UX and icons (@d4708)
- fuller debug messages (@caryoscelus)
- new contributions are GPLv3+
### zeronet-conservancy 0.7.5 (2022-05-17) (fca3c8544debd533)
maintainers: @caryoscelus , @d4708
- disable UPnP until it's proven robust
- new icon & minor rebranding
- don't check port in tor-only mode
- documentation updates & fixes (by @caryoscelus and @d4708)
- update Android/Termux dependency list (thx to nnmnmknmki reports)
- fix compatibility with modern hashlib (affects Android/Termux & others)
- cleanup
- more active trackers
- replace old start script with a better one
- colourful greetings
### zeronet-conservancy 0.7.4 (2022-04-25) (733b1b05b1)
maintainers: @caryoscelus
- fix UiRequest.parsePath & minor code improvements
- Sidebar "Open site directory" feature (by @defder-su)
- fix multiuser/merger plugin interaction
- use new admin page
- update dependency instructions
### zeronet-conservancy 0.7.3.2 (2022-01-26) (1a73dd7)
maintainers: @caryoscelus
(quick fix: technical release with proper versioning info)
### zeronet-conservancy 0.7.3.1 (2022-01-26) (a57ebf8)
maintainers: @caryoscelus
- don't grant ADMIN permission to home or update pages
- allow granting ADMIN permission via `--admin_pages` command line option
- fix infinite readdress bug (thx @mahdi-ln for reporting)
### zeronet-conservancy 0.7.3 (2022-01-21) Rev5000
maintainers: @caryoscelus
- forked from the latest py3 branch of ZeroNet
- fixed potential vulnerability discovered by @purplesyringa
- onion v3 support (thanks to @anonymoose, @zeroseed and @geekless)
- partial readme rewrite (thanks to @mitya57)
- disable updating through zite (unsafe)
- polish translation update (by Krzysztof Otręba)
- improved install instructions
### ZeroNet 0.7.2+ (latest official py3 branch)
maintainers: shortcutme a.k.a nofish a.k.a HelloZeroNet a.k.a Tamas Kocsis
- Update requirements.txt (#2617) (Jabba)
- Fix Cors permission request for connecting site
- Allow sites to request several CORS permissions at once (#2631) (Ivanq)
- Display library versions at /Env url endpoint
- Fix OpenSSL dll/so location find patcher
- Fix site listing show on big site visit
- Fix 404 error handler in UiFilePlugin (Ivanq)
- other fixes, improvements and translation updates (see more in git log)
### ZeroNet 0.7.2 2020-09-21 (Rev4528)
maintainers: shortcutme a.k.a nofish a.k.a HelloZeroNet a.k.a Tamas Kocsis
- Save content.json of site even if limit size is reached (#2114) (Lola Dam)
- fix #2107; Still save the content.json received even if site size limit is reached but dont download files (Lola Dam)
- Add multiuser admin status to server info
- Restrict blocked site addition when using mergerSiteAdd
- Open console with #ZeroNet:Console hash in url
- Fix compacting large json files
- Fix utf8 post data parsing
- Remove UiRequestPlugin from Zeroname plugin
- Fix shutdown errors on macOS
- Fix OpenSSL cert generation using LibreSSL
- Allow NOSANDBOX in local mode (#2238) (Filip Š)
- Remove limitations for img, font, media, style src in raw mode
- Use master seed to create new site from cli
- No restart after update (#2242) (Lola Dam)
- Upgrade to Python 3.8 (Christian Clauss)
- Allow all valid filenames to be added to content.json (#2141) (Josh)
- Fix loading screen scrolling on smaller screens
- Fix file rendering if content.json download failed
- Replace usage of deprecated API 'cgi.parse_qsl' (Natalia Fenclová)
- Handle announcer thread killing properly
- Move file writes and reads to separate thread
- Allow images from data uris
- Prefer connecting to non-onion peers
- Make sure we use local peers if possible
- Faster, async local ip discovery
- improve db access locks
- Fix memory leak when using sleep in threads
- more thread safety
- Validate json files in src and plugins dir
- Don't add escaping iframe message for link without target=_top
- Fix updateing deleted site in contentdb
- Don't allow parallel sites.json loading
- Fix incomplete loading of dbschema.json
- Added Custom Openssl Path for Native Clients and start_dir config (canewsin)
- Fixed "LookupError: 'hex' is not a text encoding" on /StatsBootstrapper page (#2442) (krzotr)
- Switch from gevent-websocket to gevent-ws (#2439) (Ivanq)
- Make ThreadPool a context manager to prevent memory leaks (Ivanq)
- Fix sslcrypto thread safety (#2454)
- Search for any OpenSSL version in LD_LIBRARY_PATH (Ivanq)
- Change to GPLv3 license
- Allow opening the sidebar while content.json is not loaded (Vadim Ushakov)
- UiPassword fixes
- Fix loading invalid site block list
- Fix wrapper_nonce adding to url
- Try to recover site privatekey from master seed when site owned switch enabled
- Fix not downloaded site delete on startup
- bad file download fixes
- UiFileManager plugin
- translation updates
- many other fixes and improvements (see git log for details)
### ZeroNet 0.7.1 (2019-07-01) Rev4206 ### ZeroNet 0.7.1 (2019-07-01) Rev4206
### Added ### Added
@ -20,7 +222,7 @@
- Link to site's sidebar with "#ZeroNet:OpenSidebar" hash - Link to site's sidebar with "#ZeroNet:OpenSidebar" hash
### Changed ### Changed
- Allow .. in file names [Thanks to imachug] - Allow .. in file names [Thanks to purplesyringa]
- Change unstable trackers - Change unstable trackers
- More clean errors on sites.json/users.json load error - More clean errors on sites.json/users.json load error
- Various tweaks for tracker rating on unstable connections - Various tweaks for tracker rating on unstable connections
@ -31,12 +233,12 @@
### Fixed ### Fixed
- Fix parsing config lines that have no value - Fix parsing config lines that have no value
- Fix start.py [Thanks to imachug] - Fix start.py [Thanks to purplesyringa]
- Allow multiple values of the same key in the config file [Thanks ssdifnskdjfnsdjk for reporting] - Allow multiple values of the same key in the config file [Thanks ssdifnskdjfnsdjk for reporting]
- Fix parsing config file lines that has % in the value [Thanks slrslr for reporting] - Fix parsing config file lines that has % in the value [Thanks slrslr for reporting]
- Fix bootstrapper plugin hash reloads [Thanks geekless for reporting] - Fix bootstrapper plugin hash reloads [Thanks geekless for reporting]
- Fix CryptMessage plugin OpenSSL dll loading on Windows (ZeroMail errors) [Thanks cxgreat2014 for reporting] - Fix CryptMessage plugin OpenSSL dll loading on Windows (ZeroMail errors) [Thanks cxgreat2014 for reporting]
- Fix startup error when using OpenSSL 1.1 [Thanks to imachug] - Fix startup error when using OpenSSL 1.1 [Thanks to purplesyringa]
- Fix a bug that did not loaded merged site data for 5 sec after the merged site got added - Fix a bug that did not loaded merged site data for 5 sec after the merged site got added
- Fix typo that allowed to add new plugins in public proxy mode. [Thanks styromaniac for reporting] - Fix typo that allowed to add new plugins in public proxy mode. [Thanks styromaniac for reporting]
- Fix loading non-big files with "|all" postfix [Thanks to krzotr] - Fix loading non-big files with "|all" postfix [Thanks to krzotr]
@ -59,10 +261,10 @@ Note: The fix is also back ported to ZeroNet Py 2.x version (Rev3870)
- Generated SSL certificate randomization to avoid protocol filters (Thanks to ValdikSS) - Generated SSL certificate randomization to avoid protocol filters (Thanks to ValdikSS)
- Offline mode - Offline mode
- P2P source code update using ZeroNet protocol - P2P source code update using ZeroNet protocol
- ecdsaSign/Verify commands to CryptMessage plugin (Thanks to imachug) - ecdsaSign/Verify commands to CryptMessage plugin (Thanks to purplesyringa)
- Efficient file rename: change file names instead of re-downloading the file. - Efficient file rename: change file names instead of re-downloading the file.
- Make redirect optional on site cloning (Thanks to Lola) - Make redirect optional on site cloning (Thanks to Lola)
- EccPrivToPub / EccPubToPriv functions (Thanks to imachug) - EccPrivToPub / EccPubToPriv functions (Thanks to purplesyringa)
- Detect and change dark/light theme based on OS setting (Thanks to filips123) - Detect and change dark/light theme based on OS setting (Thanks to filips123)
### Changed ### Changed
@ -81,7 +283,7 @@ Note: The fix is also back ported to ZeroNet Py 2.x version (Rev3870)
- Fix site download as zip file - Fix site download as zip file
- Fix displaying sites with utf8 title - Fix displaying sites with utf8 title
- Error message if dbRebuild fails (Thanks to Lola) - Error message if dbRebuild fails (Thanks to Lola)
- Fix browser reopen if executing start.py again. (Thanks to imachug) - Fix browser reopen if executing start.py again. (Thanks to purplesyringa)
### ZeroNet 0.6.5 (2019-02-16) Rev3851 (Last release targeting Python 2.7.x) ### ZeroNet 0.6.5 (2019-02-16) Rev3851 (Last release targeting Python 2.7.x)
@ -170,7 +372,7 @@ Affected versions: All versions before ZeroNet Rev3616
- Detect network level tracker blocking and easy setting meek proxy for tracker connections. - Detect network level tracker blocking and easy setting meek proxy for tracker connections.
- Support downloading 2GB+ sites as .zip (Thx to Radtoo) - Support downloading 2GB+ sites as .zip (Thx to Radtoo)
- Support ZeroNet as a transparent proxy (Thx to JeremyRand) - Support ZeroNet as a transparent proxy (Thx to JeremyRand)
- Allow fileQuery as CORS command (Thx to imachug) - Allow fileQuery as CORS command (Thx to purplesyringa)
- Windows distribution includes Tor and meek client by default - Windows distribution includes Tor and meek client by default
- Download sites as zip link to sidebar - Download sites as zip link to sidebar
- File server port randomization - File server port randomization
@ -233,7 +435,7 @@ Affected versions: All versions before ZeroNet Rev3616
### Added ### Added
- New plugin: Chart - New plugin: Chart
- Collect and display charts about your contribution to ZeroNet network - Collect and display charts about your contribution to ZeroNet network
- Allow list as argument replacement in sql queries. (Thanks to imachug) - Allow list as argument replacement in sql queries. (Thanks to purplesyringa)
- Newsfeed query time statistics (Click on "From XX sites in X.Xs on ZeroHello) - Newsfeed query time statistics (Click on "From XX sites in X.Xs on ZeroHello)
- New UiWebsocket API command: As to run commands as other site - New UiWebsocket API command: As to run commands as other site
- Ranged ajax queries for big files - Ranged ajax queries for big files
@ -254,7 +456,7 @@ Affected versions: All versions before ZeroNet Rev3616
- Only zoom sidebar globe if mouse button is pressed down - Only zoom sidebar globe if mouse button is pressed down
### Fixed ### Fixed
- Open port checking error reporting (Thanks to imachug) - Open port checking error reporting (Thanks to purplesyringa)
- Out-of-range big file requests - Out-of-range big file requests
- Don't output errors happened on gevent greenlets twice - Don't output errors happened on gevent greenlets twice
- Newsfeed skip sites with no database - Newsfeed skip sites with no database
@ -334,7 +536,7 @@ Affected versions: All versions before ZeroNet Rev3616
- Opened port checking (Thanks l5h5t7 & saber28 for reporting) - Opened port checking (Thanks l5h5t7 & saber28 for reporting)
- Standalone update.py argument parsing (Thanks Zalex for reporting) - Standalone update.py argument parsing (Thanks Zalex for reporting)
- uPnP crash on startup (Thanks Vertux for reporting) - uPnP crash on startup (Thanks Vertux for reporting)
- CoffeeScript 1.12.6 compatibility (Thanks kavamaken & imachug) - CoffeeScript 1.12.6 compatibility (Thanks kavamaken & purplesyringa)
- Multi value argument parsing - Multi value argument parsing
- Database error when running from directory that contains special characters (Thanks Pupiloho for reporting) - Database error when running from directory that contains special characters (Thanks Pupiloho for reporting)
- Site lock violation logging - Site lock violation logging

View file

@ -1,33 +0,0 @@
FROM alpine:3.11
#Base settings
ENV HOME /root
COPY requirements.txt /root/requirements.txt
#Install ZeroNet
RUN apk --update --no-cache --no-progress add python3 python3-dev gcc libffi-dev musl-dev make tor openssl \
&& pip3 install -r /root/requirements.txt \
&& apk del python3-dev gcc libffi-dev musl-dev make \
&& echo "ControlPort 9051" >> /etc/tor/torrc \
&& echo "CookieAuthentication 1" >> /etc/tor/torrc
RUN python3 -V \
&& python3 -m pip list \
&& tor --version \
&& openssl version
#Add Zeronet source
COPY . /root
VOLUME /root/data
#Control if Tor proxy is started
ENV ENABLE_TOR false
WORKDIR /root
#Set upstart command
CMD (! ${ENABLE_TOR} || tor&) && python3 zeronet.py --ui_ip 0.0.0.0 --fileserver_port 26552
#Expose ports
EXPOSE 43110 26552

View file

@ -1,34 +0,0 @@
FROM alpine:3.12
#Base settings
ENV HOME /root
COPY requirements.txt /root/requirements.txt
#Install ZeroNet
RUN apk --update --no-cache --no-progress add python3 python3-dev gcc libffi-dev musl-dev make tor openssl \
&& pip3 install -r /root/requirements.txt \
&& apk del python3-dev gcc libffi-dev musl-dev make \
&& echo "ControlPort 9051" >> /etc/tor/torrc \
&& echo "CookieAuthentication 1" >> /etc/tor/torrc
RUN python3 -V \
&& python3 -m pip list \
&& tor --version \
&& openssl version
#Add Zeronet source
COPY . /root
VOLUME /root/data
#Control if Tor proxy is started
ENV ENABLE_TOR false
WORKDIR /root
#Set upstart command
CMD (! ${ENABLE_TOR} || tor&) && python3 zeronet.py --ui_ip 0.0.0.0 --fileserver_port 26552
#Expose ports
EXPOSE 43110 26552

11
LICENSE
View file

@ -11,7 +11,16 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
Additional Conditions : All contributions after commit 2a4927b77b55dea9c40d14ce2cfa8a0bf0a90b80
(and/or made after 2022-05-22 UTC 00:00:00) can additionally be used
under GNU General Public License as published by the Free Software
Foundation, version 3 or (at your option) any later version.
Additional Conditions (please note that these are likely to not be legally bounding,
but before this is cleared we're leaving this note) applying to contributions from
commit 1de748585846e935d9d88bc7f22c69c84f7b8252 till commit
2a4927b77b55dea9c40d14ce2cfa8a0bf0a90b80:
Contributing to this repo Contributing to this repo
This repo is governed by GPLv3, same is located at the root of the ZeroNet git repo, This repo is governed by GPLv3, same is located at the root of the ZeroNet git repo,

274
README-ja.md Normal file
View file

@ -0,0 +1,274 @@
# zeronet-conservancy
[![Packaging status](https://repology.org/badge/vertical-allrepos/zeronet-conservancy.svg)](https://repology.org/project/zeronet-conservancy/versions)
(このファイルの翻訳は通常このファイルの後ろにあります)
[по-русски](README-ru.md) | [em português](README-ptbr.md) | [简体中文](README-zh-cn.md) | 日本語
`zeronet-conservancy`は[ZeroNet](https://github.com/HelloZeroNet/ZeroNet)プロジェクトのフォーク/継続です
その作成者によって放棄された、既存のp2pネットワークを維持し、開発することに専念しています
分散化と自由の価値観を持ちながら、徐々により良い設計のネットワークに移行していきます
## アクティブなメンテナーの警告なし
このフォークは@caryoscelusによって作成および維持されましたが、興味が薄れたため、
もう一人のプロジェクトを持つことを避けるために、開発は制限されています。
## なぜフォークするのか?
onion-v3スイッチの危機の間、私たちはonion-v3で動作し、1人または
2人の人々への信頼に依存しないフォークが必要でした。このフォークはその使命を果たすことから始まりました
誰でも簡単に監査できる[ZeroNet/py3](https://github.com/HelloZeroNet/ZeroNet/tree/py3)ブランチに最小限の変更を加えます。
フォークの初期リリースを使用してonion-v3を動作させることはまだできますが、このフォークの目標はそれ以来シフトしました
新しい、完全に透明で監査されたネットワークが準備が整い、このプロジェクトが休止できるまで、より多くの問題を解決し、ユーザーエクスペリエンスとセキュリティを全体的に改善することに専念しています
## なぜ0netなのか
* 私たちは、オープンで自由で検閲されていないネットワークとコミュニケーションを信じています。
* 単一障害点がない少なくとも1つのピアが
それを提供しています。
* ホスティングコストなし:サイトは訪問者によって提供されます。
* シャットダウンすることは不可能です:どこにもないのでどこにでもあります。
* 高速でオフラインで動作します:インターネットが利用できない場合でもサイトにアクセスできます。
## 特徴
* リアルタイムで更新されるサイト
* ワンクリックでウェブサイトをクローン
* プライベート/パブリックキーを使用したパスワードレス認証
* P2Pデータ同期を備えた組み込みSQLサーバーより簡単な動的サイト開発を可能にします
* 匿名性:.onion隠しサービスを使用したTorネットワークのサポートonion-v3サポートを含む
* TLS暗号化接続クリーンネット経由
* 自動uPnPポート開放オプトインした場合
* マルチユーザー(オープンプロキシ)サポート用のプラグイン
* すべてのモダンブラウザ/OSで動作
* オフラインで動作し、代替トランスポートを介して同期できます(または接続が戻ったとき)
## どのように機能しますか?
* `zeronet.py`を起動した後、zeronetサイトにアクセスできるようになります
`http://127.0.0.1:43110/{zeronet_address}`(例:
`http://127.0.0.1:43110/1MCoA8rQHhwu4LY2t2aabqcGSRqrL8uf2X/`)。
* 新しいzeronetサイトにアクセスすると、BitTorrentを使用してピアを見つけようとします
ネットワークは、サイトファイルhtml、css、js ...)をそれらからダウンロードできるようにします。
* 訪問した各サイトもあなたによって提供されます。
* すべてのサイトには、すべての他のファイルをsha512ハッシュで保持する`content.json`ファイルが含まれています
およびサイトの秘密鍵を使用して生成された署名。
* サイト所有者(サイトアドレスの秘密鍵を持っている人)がサイトを変更した場合
サイト、次に彼/彼女は新しい`content.json`に署名し、それをピアに公開します。
その後、ピアは`content.json`の整合性を確認します(
署名)、変更されたファイルをダウンロードし、新しいコンテンツを
他のピア。
次のリンクは、元のZeroNetに関連しています。
- [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/)(古くなっています)
## 参加方法
### ディストリビューションリポジトリからインストール
- NixOS[zeronet-conservancyパッケージ検索](https://search.nixos.org/packages?from=0&size=50&sort=relevance&type=packages&query=zeronet-conservancy)(以下を参照)
- ArchLinux[最新リリース](https://aur.archlinux.org/packages/zeronet-conservancy)、[最新のgitバージョン](https://aur.archlinux.org/packages/zeronet-conservancy-git)
### NixパッケージマネージャーからインストールLinuxまたはMacOS
- nixパッケージマネージャーをインストールして構成します必要に応じて
- `nix-env -iA nixpkgs.zeronet-conservancy`
または、NixOSを使用している場合は、システム構成に`zeronet-conservancy`を追加します
(パッケージの作成と維持を行ってくれた@fgazに感謝します
### ソースからインストール
#### システム依存関係
##### 一般的なUnix系mac os xを含む
autoconfおよびその他の基本的な開発ツール、python3およびpipをインストールし、「ビルドpython依存関係」に進みます
(実行に失敗した場合は、欠落している依存関係を報告するか、依存関係リストを修正するためのプルリクエストを作成してください)
##### Aptベースdebian、ubuntuなど
- `sudo apt update`
- `sudo apt install git pkg-config libffi-dev python3-pip python3-venv python3-dev build-essential libtool`
##### Red HatおよびFedoraベース
- `yum install epel-release -y 2>/dev/null`
- `yum install git python3 python3-wheel`
##### Fedoraベースのdandified
- `sudo dnf install git python3-pip python3-wheel -y`
##### openSUSE
- `sudo zypper install python3-pip python3-setuptools python3-wheel`
##### ArchおよびManjaroベース
- `sudo pacman -S git python-pip -v --no-confirm`
##### Android/Termux
- [Termux](https://termux.com/)をインストールしますTermuxでは、`pkg install <package-names>`を使用してパッケージをインストールできます)
- `pkg update`
- `pkg install python automake git binutils libtool`
- 古いandroidバージョンでは、`pkg install openssl-tool libcrypt clang`もインストールする必要がある場合があります)
- (上記のパッケージをインストールしても起動の問題が発生する場合は、報告してください)
- (オプション)`pkg install tor`
- (オプション)`tor --ControlPort 9051 --CookieAuthentication 1`コマンドを使用してtorを実行します右にスワイプして新しいセッションを開くことができます
#### python依存関係、venvのビルドと実行
- このリポジトリをクローンしますAndroid/Termuxでは、仮想環境が`storage/`に存在できないため、Termuxの「ホーム」フォルダにクローンする必要があります
- `python3 -m venv venv`python仮想環境を作成します。最後の`venv`は名前にすぎません。後のコマンドで置き換える必要がある場合は、別の名前を使用してください)
- `source venv/bin/activate`(環境をアクティブ化)
- `python3 -m pip install -r requirements.txt`(依存関係をインストール)
- `python3 zeronet.py`**zeronet-conservancyを実行**
- ブラウザでランディングページを開き、http://127.0.0.1:43110/ に移動します
- 新しいターミナルから再起動するには、リポジトリディレクトリに移動して次のコマンドを実行する必要があります:
- `source venv/bin/activate`
- `python3 zeronet.py`
#### 代替NixOSで
- このリポジトリをクローン
- `nix-shell '<nixpkgs>' -A zeronet-conservancy`を実行して、依存関係がインストールされたシェルに入ります
- `./zeronet.py`
#### 代替Dockerイメージのビルド
- 0netイメージをビルド`docker build -t 0net-conservancy:latest . -f Dockerfile`
- 統合されたtorを使用して0netイメージをビルド`docker build -t 0net-conservancy:latest . -f Dockerfile.integrated_tor`
- 実行:`docker run --rm -it -v </path/to/0n/data/directory>:/app/data -p 43110:43110 -p 26552:26552 0net-conservancy:latest`
- /path/to/0n/data/directory - すべてのデータが保存されるディレクトリ。秘密の証明書も含まれます。運用モードで実行する場合は、このフォルダを削除しないでください!
- または、docker-composeを使用して実行できます`docker compose up -d 0net-conservancy`は、0netとtorの2つのコンテナを個別に起動します。
- または:`docker compose up -d 0net-tor`を実行して、1つのコンテナで0netとtorを実行します。
(これらの手順がまだ正確かどうかを確認してください)
#### 代替のワンライナー(@ssdifnskdjfnsdjkによるpython依存関係をグローバルにインストール
Githubリポジトリをクローンし、必要なPythonモジュールをインストールします。最初に
コマンドの先頭にあるzndirパスを、`zeronet-conservancy`を保存するパスに編集します:
`zndir="/home/user/myapps/zeronet" ; if [[ ! -d "$zndir" ]]; then git clone --recursive "https://github.com/zeronet-conservancy/zeronet-conservancy.git" "$zndir" && cd "$zndir"||exit; else cd "$zndir";git pull origin master; fi; cd "$zndir" && pip install -r requirements.txt|grep -v "already satisfied"; echo "Try to run: python3 $(pwd)/zeronet.py"`
(このコマンドは、`zeronet-conservancy`を最新の状態に保つためにも使用できます)
#### 代替スクリプト
- 一般的な依存関係をインストールし、リポジトリをクローンした後(上記のように)、
`start-venv.sh`を実行して仮想環境を作成し、
pythonの要件をインストールします
- 便利なスクリプトを近日追加予定
### 非公式Windows OSビルド
.zipアーカイブをダウンロードして解凍します
[zeronet-conservancy-0.7.10-unofficial-win64.zip](https://github.com/zeronet-conservancy/zeronet-conservancy/releases/download/v0.7.10/zeronet-conservancy-0.7.10-unofficial-win64.zip)
### Windows OSでのビルド
(これらの手順は進行中の作業です。テストして改善するのに役立ててください!)
- https://www.python.org/downloads/ からpythonをインストールします
- pythonに適したWindowsコンパイラをインストールします。これが私にとって最も難しい部分でしたhttps://wiki.python.org/moin/WindowsCompilers を参照し、後でさらに参考資料をリンクします)
- [最新の開発バージョンを取得するためにオプション] https://git-scm.com/downloads からgitをインストールします
- [より良い接続性と匿名性のためにtorを使用するためにオプション] https://www.torproject.org/download/ からtorブラウザをインストールします
- git bashコンソールを開きます
- コマンドラインに`git clone https://github.com/zeronet-conservancy/zeronet-conservancy.git`を入力/コピーします
- gitが最新の開発バージョンをダウンロードするのを待ち、コンソールで続行します
- `cd zeronet-conservancy`
- `python -m venv venv`仮想python環境を作成します
- `venv\Scripts\activate`(これにより環境がアクティブになります)
- `pip install -r requirements.txt`python依存関係をインストールします一部のユーザーは、このコマンドが依存関係を正常にインストールせず、依存関係を1つずつ手動でインストールする必要があると報告しています
- 前のステップが失敗した場合、c/c++コンパイラが正常にインストールされていない可能性が高いです)
- [より良い接続性と匿名性のためにtorを使用するためにオプション] Torブラウザを起動します
- Windowsは「セキュリティ上の理由から」インターネットへのアクセスをブロックしたというウィンドウを表示する場合があります。アクセスを許可する必要があります
- `python zeronet.py --tor_proxy 127.0.0.1:9150 --tor_controller 127.0.0.1:9151`zeronet-conservancyを起動
- [完全なtor匿名性のためにこれを起動] `python zeronet.py --tor_proxy 127.0.0.1:9150 --tor_controller 127.0.0.1:9151 --tor always`
- お気に入りのブラウザでhttp://127.0.0.1:43110 に移動します!
.exeをビルドするには
- 上記と同じ手順に従いますが、さらに
- `pip install pyinstaller`
- `pyinstaller -p src -p plugins --hidden-import merkletools --hidden-import lib.bencode_open --hidden-import Crypt.Crypt --hidden-import Db.DbQuery --hidden-import lib.subtl --hidden-import lib.subtl.subtl --hidden-import sockshandler --add-data "src;src" --add-data "plugins;plugins" --clean zeronet.py`
- dist/zeronetには動作するzeronet.exeが含まれているはずです
## 現在の制限
* ファイルトランザクションは圧縮されません
* プライベートサイトなし
* DHTサポートなし
* I2Pサポートなし
* zeroidのような集中化された要素これに取り組んでいます
* 信頼できるスパム保護なし(これにも取り組んでいます)
* ブラウザから直接動作しません中期的な優先事項の1つ
* データの透明性なし
* 再現可能なビルドなし
* オンディスク暗号化なし
* 再現可能なビルドなししたがって、特定のGNU/Linuxディストリビューションを超えたビルドはありません
## ZeroNetサイトを作成するにはどうすればよいですか
* [ダッシュボード](http://127.0.0.1:43110/191CazMVNaAcT9Y1zhkxd9ixMBPs59g2um/)の**⋮** > **「新しい空のサイトを作成」**メニュー項目をクリックします。
* **リダイレクト**され、あなただけが変更できる完全に新しいサイトに移動します!
* **data/[yoursiteaddress]**ディレクトリでサイトのコンテンツを見つけて変更できます
* 変更後にサイトを開き、右上の「0」ボタンを左にドラッグしてから、下部の**署名して公開**ボタンを押します
次のステップ:[ZeroNet開発者ドキュメント](https://zeronet.io/docs/site_development/getting_started/)
## このプロジェクトを存続させるために支援する
### メンテナーになる
もっとメンテナーが必要です今日1つになりましょうコーディング方法を知る必要はありません、
他にもたくさんの仕事があります。
### プラットフォーム用のビルドを作成する
主要なプラットフォーム用の再現可能なスタンドアロンビルド、およびさまざまなFLOSSリポジトリへのプレゼンスが必要です。まだパッケージがないLinuxディストリビューションの1つを使用している場合は、パッケージを作成するか方法がわからない場合は今すぐメンテナーに依頼してみませんか
### バグを修正し、機能を追加する
私たちは前進し、完璧なp2pウェブを作成することにしました。したがって、実装を支援するためにさらに多くの支援が必要です。
### サイトを作成する/コンテンツを持ち込む
ドキュメントが不足していることはわかっていますが、できる限りのサポートを提供しようとしています
移行したい人。遠慮なく質問してください。
### 使用して広める
なぜ0netとこのフォークを特に使用するのかを人々に必ず伝えてください人々
彼らの選択肢を知る必要があります。
### メンテナーを財政的に支援する
このフォークは@caryoscelusによって作成および維持されました。あなたは
https://caryoscelus.github.io/donate/ で彼らに寄付する方法を確認してください(または
githubでこれを読んでいる場合は、他の方法についてはサイドバーを確認してください。私たちのチームが成長するにつれて、私たちは
フレンドリーなクラウドファンディングプラットフォームにチームアカウントを作成します。
このプロジェクトへの寄付として寄付が認識されることを確認したい場合は、
専用のビットコインアドレスもあります:
1Kjuw3reZvxRVNs27Gen7jPJYCn6LY7Fg6。より匿名でプライベートにしたい場合は、Moneroウォレット
4AiYUcqVRH4C2CVr9zbBdkhRnJnHiJoypHEsq4N7mQziGUoosPCpPeg8SPr87nvwypaRzDgMHEbWWDekKtq8hm9LBmgcMzC
別の方法で寄付したい場合は、メンテナーに連絡するか、
問題を作成する
# 抗議の下でGitHubを使用しています
このプロジェクトは現在GitHubでホストされています。これは理想的ではありません。 GitHubは
フリー/リブレおよびオープンソースソフトウェアではない独自のトレードシークレットシステム
FLOSS。 GitHubのような独自のシステムを使用してFLOSSプロジェクトを開発することについて深く懸念しています。私たちは
長期的にGitHubから移行するための[オープンイシュー](https://github.com/zeronet-conservancy/zeronet-conservancy/issues/89)があります。 [Give up GitHub](https://GiveUpGitHub.org)キャンペーンについて読むことをお勧めします
[ソフトウェアフリーダムコンセルバンシー](https://sfconservancy.org)から
GitHubがFOSSプロジェクトをホストするのに適していない理由のいくつかを理解するため。
すでにGitHubの使用を個人的にやめたコントリビューターの場合は、
[notabugのミラーからチェックアウト](https://notabug.org/caryoscelus/zeronet-conservancy)
して、そこで開発するか、gitパッチをプロジェクトメンテナーに直接送信します
[連絡先チャネル](https://caryoscelus.github.io/contacts/)を好む。
GitHub Copilotによるこのプロジェクトのコードの過去または現在の使用は、
私たちの許可なしに行われます。私たちは、Copilotでこのプロジェクトのコードを使用することに同意しません。
![GiveUpGitHubキャンペーンのロゴ](https://sfconservancy.org/img/GiveUpGitHub.png)

164
README-ptbr.md Normal file
View file

@ -0,0 +1,164 @@
# zeronet-conservancy
[in English](README.md) | [по-русски](README-ru.md) | [简体中文](README-zh-cn.md) | [日本語](README-ja.md)
[![Packaging status](https://repology.org/badge/vertical-allrepos/zeronet-conservancy.svg)](https://repology.org/project/zeronet-conservancy/versions)
zeronet-conservancy é um garfo/continuação do projeto [ZeroNet](https://github.com/HelloZeroNet/ZeroNet)
(que foi abandonada por seu criador) que se dedica a sustentar a rede p2p existente e a desenvolver
seus valores de descentralização e liberdade, enquanto muda gradualmente para uma rede mais bem projetada
## Por que garfo?
Durante a crise da onion-v3, precisávamos de um garfo que funcionasse com onion-v3 e não dependesse da confiança de um ou
duas pessoas. Este garfo começou a partir do cumprimento dessa missão, implementando mudanças mínimas para
[ZeroNet/py3](https://github.com/HelloZeroNet/ZeroNet/tree/py3) ramo que é fácil de ser auditado por qualquer pessoa. Enquanto
você ainda pode usar as primeiras liberações do garfo para fazer funcionar a onion-v3, o objetivo deste garfo mudou desde então
e nos dedicamos a resolver mais problemas e melhorar a experiência do usuário e a segurança por toda parte, até
a nova rede, completamente transparente e auditada está pronta e este projeto pode ser colocado para descansar
## Por que 0net?
* Acreditamos em redes e comunicação abertas, livres e não censuradas.
* Nenhum ponto único de falha: O site permanece online desde que pelo menos 1 par seja
servindo-o.
* Sem custos de hospedagem: Os sites são servidos por visitantes.
* Impossível de ser desligado: Não está em lugar nenhum porque está em toda parte.
* Rápido e funciona offline: Você pode acessar o site, mesmo que a Internet seja
indisponível.
## Características
* Sites atualizados em tempo real
* Clonar websites em um clique
* Autorização sem senha usando chaves públicas/privadas
* Servidor SQL integrado com sincronização de dados P2P: permite um desenvolvimento dinâmico mais fácil do site
* Anonimato: Suporte de rede Tor com serviços ocultos .onion (incluindo suporte a onion-v3)
* conexões criptografadas TLS (através de clearnet)
* Abertura automática da porta uPnP (se optado por entrar)
* Plugin para suporte multiusuário (openproxy)
* Funciona com qualquer navegador/OS moderno
## Como funciona?
* Após iniciar o `zeronet.py` você poderá visitar os sites da zeronet utilizando
`http://127.0.0.1:43110/{zeronet_address}` (ex.
`http://127.0.0.1:43110/126NXcevn1AUehWFZLTBw7FrX1crEizQdr`).
* Quando você visita um novo site zeronet, ele tenta encontrar pares usando o BitTorrent
para poder baixar os arquivos do site (html, css, js...) a partir deles.
* Cada site visitado também é servido por você.
* Cada site contém um arquivo `content.json` que contém todos os outros arquivos em um hash sha512
e uma assinatura gerada usando a chave privada do site.
* Se o proprietário do site (que tem a chave privada para o endereço do site) modificar o
então ele assina o novo `content.json` e o publica para os colegas.
Em seguida, os pares verificam a integridade do `content.json` (utilizando o
assinatura), eles baixam os arquivos modificados e publicam o novo conteúdo para
outros colegas.
Os links a seguir referem-se à ZeroNet original:
- [Slideshow sobre criptografia ZeroNet, atualizações de sites, sites multiusuário "](https://docs.google.com/presentation/d/1_2qK1IuOKJ51pgBvllZ9Yu7Au2l551t3XBgyTSvilew/pub?start=false&loop=false&delayms=3000)
- [Perguntas mais freqüentes "](https://zeronet.io/docs/faq/)
- [Documentação do Desenvolvedor da ZeroNet "](https://zeronet.io/docs/site_development/getting_started/)
## Como aderir
### Instalar a partir da fonte (recomendado)
#### Dependências do sistema
##### Genéricos unix-like (incluindo mac os x)
Instalar o autoconf e outras ferramentas básicas de desenvolvimento, python3 e pip.
##### Apt-based (debian, ubuntu, etc)
- `sudo apt update`
- `sudo apt install pkg-config python3-pip python3-venv`
##### Android/Termux
- install [Termux](https://termux.com/) (no Termux você pode instalar pacotes via `pkg install <nomes de pacotes>`)
- Atualização do "pkg".
- `pkg install python automake git binutils libtool` (TODO: verificar nova instalação se há mais dependências para instalar)
- (opcional) `pkg install tor`
- (opcional) rodar tor via comando `tor --ControlPort 9051 --CookieAuthentication 1` (você pode então abrir uma nova sessão deslizando para a direita)
#### Construindo dependências python & running
- clonar este repo (NOTA: no Android/Termux você deve cloná-lo na pasta "home" do Termux, porque o ambiente virtual não pode viver no `storage/`)
- "python3 -m venv venv" (fazer python ambiente virtual, o último `venv` é apenas um nome, se você usar diferente você deve substituí-lo em comandos posteriores)
- "fonte venv/bin/activate" (activar ambiente)
- `python3 -m pip install -r requirements.txt` (instalar dependências)
- zeronet.py` (**run zeronet-conservancy!**)
- abra a página de desembarque em seu navegador navegando para: http://127.0.0.1:43110/
- para reiniciá-lo a partir de um terminal novo, você precisa navegar para redirecionar o diretório e:
- "fonte venv/bin/activate
- "python3 zeronet.py
#### Construir imagem do Docker
- construir imagem 0net: `docker build -t 0net:conservancy . -f Dockerfile`
- ou construir imagem 0net com tor integrado: `docker build -t 0net:conservancy . -f Dockerfile.integrated_tor`
- e dirigi-lo: `docker run --rm -it -v </path/to/0n/data/directório>:/app/data -p 43110:43110 -p 26552:26552 0net:conservancy''.
- /caminho/até/0n/dados/diretório - diretório, onde todos os dados serão salvos, incluindo seus certificados secretos. Se você executá-lo com o modo de produção, não remova esta pasta!
- ou você pode executá-lo com o docker-compose: `docker compose up -d 0net` sobe dois containers - 0net e tor separadamente.
- ou: "docker compose up -d 0net-tor" para rodar 0net e tor em um recipiente.
#### roteiro alternativo
- após instalar as dependências gerais e clonagem repo (como acima), execute `start-venv.sh` que criará um ambiente virtual para você e instalará os requisitos python
- mais roteiros de conveniência a serem adicionados em breve
## Limitações atuais
* As transações de arquivos não são comprimidas
* Sem sites privados
* Sem suporte de DHT
* Elementos centralizados como o zeroid (estamos trabalhando nisso!)
* Nenhuma proteção confiável contra spam (e nisto também)
* Não funciona diretamente do navegador (uma das principais prioridades para o futuro médio)
* Sem transparência de dados
## Como posso criar um site ZeroNet?
Clique em **⋮*** > **"Criar site novo, vazio "** item do menu [página admin](http://127.0.0.1:43110/126NXcevn1AUehWFZLTBw7FrX1crEizQdr).
* Você será **re-direcionado *** para um site completamente novo que só pode ser modificado por você!
* Você pode encontrar e modificar o conteúdo de seu site no diretório **data/[endereço de seu site]**.
* Após as modificações abrir seu site, arraste o botão superior direito "0" para a esquerda, depois pressione **sign** e **publish** botões na parte inferior
Próximos passos: [Documentação do Desenvolvedor da ZeroNet](https://zeronet.io/docs/site_development/getting_started/)
## Ajude este projeto a permanecer vivo
### Torne-se um mantenedor
Precisamos de mais mantenedores! Torne-se um hoje! Você não precisa saber como codificar,
há muito mais trabalho a ser feito.
### Corrigir bugs e adicionar recursos
Decidimos ir em frente e fazer uma web p2p perfeita, então precisamos de mais ajuda
implementando-o.
#### Faça seu site/bring seu conteúdo
Sabemos que a documentação está faltando, mas tentamos o melhor para apoiar qualquer um
que quer migrar. Não hesite em perguntar.
#### Use-o e espalhe a palavra
Certifique-se de dizer às pessoas por que você usa 0net e este garfo em particular! Pessoas
precisam conhecer suas alternativas.
### Mantenedores de suporte financeiro
Atualmente, o principal desenvolvedor/mantenedor deste garfo é @caryoscelus. Você pode
veja maneiras de doar para eles em https://caryoscelus.github.io/donate/ (ou verifique
sidebar se você estiver lendo isto no github para mais maneiras). À medida que nossa equipe cresce, nós
também criará contas de equipe em plataformas amigáveis de financiamento de multidões.
Se você quiser ter certeza de que sua doação é reconhecida como doação para isto
projeto, também há um endereço dedicado ao bitcoin para isso:
1Kjuw3reZvxRVNs27Gen7jPJYCn6LY7Fg6
Se você quiser doar de uma maneira diferente, sinta-se à vontade para contatar o mantenedor ou
criar uma publicação!

View file

@ -1,40 +1,48 @@
# 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-conservancy
[简体中文](./README-zh-cn.md) [in English](README.md) | [em português](README-ptbr.md) | [简体中文](README-zh-cn.md) | [日本語](README-ja.md)
[English](./README.md)
Децентрализованные вебсайты использующие Bitcoin криптографию и BitTorrent сеть - https://zeronet.io [![Packaging status](https://repology.org/badge/vertical-allrepos/zeronet-conservancy.svg)](https://repology.org/project/zeronet-conservancy/versions)
zeronet-conservancy — это форк/продолжение проекта [ZeroNet](https://github.com/HelloZeroNet/ZeroNet)
(покинутого его создателем), предназначенный для поддержки существующей сети p2p и развития
идей ценности децентрализации и свободы, постепенно развивающийся в более совершенную сеть
## Зачем нужен этот форк?
Во время кризиса onion-v3 появилась необходимость в форке, который работал бы с onion-v3 и не зависел от доверия к конкретным личностям.
Для выполнения этой задачи форк начался с внесения минимальных изменений в
[ZeroNet/py3](https://github.com/HelloZeroNet/ZeroNet/tree/py3), которые легко проверяются. В то время как остается возможность использования ранних версий форка для работы с onion-v3, цель данного форка изменилась и мы стали стремиться решать больше проблем и повышать удобность и безопасность для пользователей до тех пор, пока новая, полностью прозрачная и проверенная сеть не будет готова, и необходимость в этом проекте не отпадет.
## Зачем? ## Зачем нужен 0net?
* Мы верим в открытую, свободную, и не отцензуренную сеть и коммуникацию. * Мы верим в открытую, свободную, и не поддающуюся цензуре сеть и коммуникацию.
* Нет единой точки отказа: Сайт онлайн пока по крайней мере 1 пир обслуживает его. * Нет единой точки отказа: Сайт онлайн пока по крайней мере 1 пир обслуживает его.
* Никаких затрат на хостинг: Сайты обслуживаются посетителями. * Никаких затрат на хостинг: Сайты обслуживаются посетителями.
* Невозможно отключить: Он нигде, потому что он везде. * Невозможно отключить: Он нигде, потому что он везде.
* Быстр и работает оффлайн: Вы можете получить доступ к сайту, даже если Интернет недоступен. * Быстр и работает оффлайн: Вы можете получить доступ к сайту, даже если Интернет недоступен.
## Особенности ## Особенности
* Обновляемые в реальном времени сайты * Обновляемые в реальном времени сайты
* Поддержка Namecoin .bit доменов
* Лёгок в установке: распаковал & запустил
* Клонирование вебсайтов в один клик * Клонирование вебсайтов в один клик
* Password-less [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) * Авторизация без паролей, с использованием пары публичный/приватный ключ
based authorization: Ваша учетная запись защищена той же криптографией, что и ваш Bitcoin-кошелек * Встроенный SQL-сервер с синхронизацией данных P2P: позволяет упростить разработку сайта
* Встроенный SQL-сервер с синхронизацией данных P2P: Позволяет упростить разработку сайта и ускорить загрузку страницы * Анонимность: поддержка сети Tor с помощью скрытых служб .onion (включая onion-v3)
* Анонимность: Полная поддержка сети Tor с помощью скрытых служб .onion вместо адресов IPv4 * TLS зашифрованные связи (в клирнете)
* TLS зашифрованные связи * Автоматическое открытие uPnP порта (опционально)
* Автоматическое открытие uPnP порта
* Плагин для поддержки многопользовательской (openproxy) * Плагин для поддержки многопользовательской (openproxy)
* Работает с любыми браузерами и операционными системами * Работает с любыми браузерами и операционными системами
## Как это работает? ## Как это работает?
* После запуска `zeronet.py` вы сможете посетить зайты (zeronet сайты) используя адрес * После запуска `zeronet.py` вы сможете посетить zeronet сайты используя адрес
`http://127.0.0.1:43110/{zeronet_address}` `http://127.0.0.1:43110/{zeronet_address}`
(например. `http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D`). (например. `http://127.0.0.1:43110/1MCoA8rQHhwu4LY2t2aabqcGSRqrL8uf2X`).
* Когда вы посещаете новый сайт zeronet, он пытается найти пиров с помощью BitTorrent * Когда вы посещаете новый сайт zeronet, он пытается найти пиров с помощью BitTorrent
чтобы загрузить файлы сайтов (html, css, js ...) из них. чтобы загрузить файлы сайтов (html, css, js ...) из них.
* Каждый посещенный зайт также обслуживается вами. (Т.е хранится у вас на компьютере) * Каждый посещенный зайт также обслуживается вами. (Т.е хранится у вас на компьютере)
@ -44,168 +52,117 @@
подписывает новый `content.json` и публикует его для пиров. После этого пиры проверяют целостность `content.json` подписывает новый `content.json` и публикует его для пиров. После этого пиры проверяют целостность `content.json`
(используя подпись), они загружают измененные файлы и публикуют новый контент для других пиров. (используя подпись), они загружают измененные файлы и публикуют новый контент для других пиров.
Ссылки c информацией о ZeroNet:
#### [Слайд-шоу о криптографии ZeroNet, обновлениях сайтов, многопользовательских сайтах »](https://docs.google.com/presentation/d/1_2qK1IuOKJ51pgBvllZ9Yu7Au2l551t3XBgyTSvilew/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/) #### [Часто задаваемые вопросы »](https://zeronet.io/docs/faq/)
#### [Документация разработчика ZeroNet »](https://zeronet.io/docs/site_development/getting_started/) #### [Документация разработчика ZeroNet »](https://zeronet.io/docs/site_development/getting_started/)
#### [Скриншоты в ZeroNet документации »](https://zeronet.io/docs/using_zeronet/sample_sites/)
## Как присоединиться
## Скриншоты ### Установить из репозитория вашего дистрибутива
![Screenshot](https://i.imgur.com/H60OAHY.png) - NixOS: https://search.nixos.org/packages?channel=22.05&show=zeronet-conservancy&type=packages&query=zeronet-conservancy
![ZeroTalk](https://zeronet.io/docs/img/zerotalk.png) - ArchLinux: [последний релиз](https://aur.archlinux.org/packages/zeronet-conservancy), [git-версия](https://aur.archlinux.org/packages/zeronet-conservancy-git)
#### [Больше скриншотов в ZeroNet документации »](https://zeronet.io/docs/using_zeronet/sample_sites/) ### Установить из исходного кода (рекомендовано)
#### System dependencies
## Как вступить ##### Generic unix-like (including mac os x)
* Скачайте ZeroBundle пакет: Установите autoconf и другие базовые инструменты разработки, python3 и pip.
* [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 64-bit](https://github.com/HelloZeroNet/ZeroBundle/raw/master/dist/ZeroBundle-linux64.tar.gz)
* [Linux 32-bit](https://github.com/HelloZeroNet/ZeroBundle/raw/master/dist/ZeroBundle-linux32.tar.gz)
* Распакуйте где угодно
* Запустите `ZeroNet.exe` (win), `ZeroNet(.app)` (osx), `ZeroNet.sh` (linux)
### Linux терминал ##### Apt-based (debian, ubuntu, etc)
- `sudo apt update`
- `sudo apt install pkg-config python3-pip python3-venv`
* `wget https://github.com/HelloZeroNet/ZeroBundle/raw/master/dist/ZeroBundle-linux64.tar.gz` ##### Android/Termux
* `tar xvpfz ZeroBundle-linux64.tar.gz` - Установите [Termux](https://termux.com/) (в Termux вы можете устанавливать пакеты через команду `pkg install <package-names>`)
* `cd ZeroBundle` - `pkg update`
* Запустите с помощью `./ZeroNet.sh` - `pkg install python automake git binutils libtool`
- (optional) `pkg install tor`
- (optional) запустить тор через команду `tor --ControlPort 9051 --CookieAuthentication 1` (вы можете открыть новый сеанс свайпом вправо)
Он загружает последнюю версию ZeroNet, затем запускает её автоматически. #### Скрипт, который всё сделает за вас
- после установки общих зависимостей и клонирования репозитория (как указано выше) запустите `start-venv.sh` который создаст для вас виртуальную среду (если её ещё нет) и установит необходимые пакеты Python
- больше удобных скриптов будует добавлено в ближайшее время
#### Ручная установка для Debian Linux #### Установка Python-зависимостей и запуск
- клонируйте репозиторий (NOTE: на Android/Termux вы должны клонировать его в «домашнюю» папку Termux, потому что виртуальная среда не может находиться в `storage/`)
- `python3 -m venv venv` (создайте виртуальную среду python, последнее `venv` это просто имя/название, если вы используете другое, вы должны заменить его в более поздних командах.)
- `source venv/bin/activate` (активируйте среду)
- `python3 -m pip install -r requirements.txt` (установите зависимости)
- `python3 zeronet.py` (**запустите zeronet-conservancy!**)
- откройте основную страницу в браузере, перейдя по: http://127.0.0.1:43110/
- для повторного запуска с нового терминала вам нужно перейти в деректорию репозитория и ввести :
- `source venv/bin/activate`
- `python3 zeronet.py`
* `sudo apt-get update` #### (альтернативно) Создание образа Docker
* `sudo apt-get install msgpack-python python-gevent` - создание образа: `docker build -t 0net-conservancy:latest . -f Dockerfile`
* `wget https://github.com/HelloZeroNet/ZeroNet/archive/master.tar.gz` - или создрание образа с встроенным tor: `docker build -t 0net-conservancy:latest . -f Dockerfile.integrated_tor`
* `tar xvpfz master.tar.gz` - и его запуск: `docker run --rm -it -v </path/to/0n/data/directory>:/app/data -p 43110:43110 -p 26552:26552 0net-conservancy:latest`
* `cd ZeroNet-master` - /path/to/0n/data/directory - директория, куда будут сохраняться все данные в том числе секретные ключи. Если вы запускаете в боевом режиме, не потеряйте эту папку!
* Запустите с помощью `python2 zeronet.py` - или вы можете воспользоваться docker-compose: `docker compose up -d 0net-conservancy` запускает два контейнера раздельно, для 0net и tor сервисов.
* Откройте http://127.0.0.1:43110/ в вашем браузере. - или: `docker compose up -d 0net-tor` запускает один контейнер с tor и 0net.
### [Arch Linux](https://www.archlinux.org) ## Текущие ограничения
* `git clone https://aur.archlinux.org/zeronet.git` * Файловые транзакции не сжаты
* `cd zeronet`
* `makepkg -srci`
* `systemctl start zeronet`
* Откройте http://127.0.0.1:43110/ в вашем браузере.
Смотрите [ArchWiki](https://wiki.archlinux.org)'s [ZeroNet
article](https://wiki.archlinux.org/index.php/ZeroNet) для дальнейшей помощи.
### [Gentoo Linux](https://www.gentoo.org)
* [`layman -a raiagent`](https://github.com/leycec/raiagent)
* `echo '>=net-vpn/zeronet-0.5.4' >> /etc/portage/package.accept_keywords`
* *(Опционально)* Включить поддержку Tor: `echo 'net-vpn/zeronet tor' >>
/etc/portage/package.use`
* `emerge zeronet`
* `rc-service zeronet start`
* Откройте http://127.0.0.1:43110/ в вашем браузере.
Смотрите `/usr/share/doc/zeronet-*/README.gentoo.bz2` для дальнейшей помощи.
### [FreeBSD](https://www.freebsd.org/)
* `pkg install zeronet` or `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`
* Подключитесь к VM с помощью `vagrant ssh`
* `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 15441:15441 -p 127.0.0.1:43110:43110 nofish/zeronet`
* Это изображение Docker включает в себя прокси-сервер Tor, который по умолчанию отключён.
Остерегайтесь что некоторые хостинг-провайдеры могут не позволить вам запускать Tor на своих серверах.
Если вы хотите включить его,установите переменную среды `ENABLE_TOR` в` true` (по умолчанию: `false`) Например:
`docker run -d -e "ENABLE_TOR=true" -v <local_data_folder>:/root/data -p 15441:15441 -p 127.0.0.1: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/ в вашем браузере.
## Текущие ограничения
* ~~Нет torrent-похожего файла разделения для поддержки больших файлов~~ (поддержка больших файлов добавлена)
* ~~Не анонимнее чем Bittorrent~~ (добавлена встроенная поддержка Tor)
* Файловые транзакции не сжаты ~~ или незашифрованы еще ~~ (добавлено шифрование TLS)
* Нет приватных сайтов * Нет приватных сайтов
* Отсутствует поддержка DHT
* Централизованные элементы, такие как Zeroid (мы работаем над этим!)
* Нет надежной защиты от спама (в процессе разработки)
* Не работает напрямую из браузера (один из главных приоритетов в ближайшем будущем)
* Нет прозрачности данных
## Как я могу создать сайт в Zeronet? ## Как создать сайт ZeroNet?
Завершите работу zeronet, если он запущен * Нажмите на **⋮** > **"Create new, empty site"** пункт меню на [admin page](http://127.0.0.1:43110/126NXcevn1AUehWFZLTBw7FrX1crEizQdr).
* Вы будете перенаправлены **redirected** на совершенно новый сайт, который можете изменить только вы!
```bash * Вы можете найти и изменить содержимое своего сайта в каталоге **data/[yoursiteaddress]**
$ zeronet.py siteCreate * После внесения изменений откройте свой сайт, перетащите верхнюю правую кнопку «0» влево, затем нажмите кнопки **sign** и **publish** , находящиеся внизу.
...
- Site private key (Приватный ключ сайта): 23DKQpzxhbVBrAtvLEc2uvk7DZweh4qL3fn3jpM3LgHDczMK2TtYUq
- Site address (Адрес сайта): 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2
...
- Site created! (Сайт создан)
$ zeronet.py
...
```
Поздравляем, вы закончили! Теперь каждый может получить доступ к вашему зайту используя
`http://localhost:43110/13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2`
Следующие шаги: [ZeroNet Developer Documentation](https://zeronet.io/docs/site_development/getting_started/) Следующие шаги: [ZeroNet Developer Documentation](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....
```
* Вот и всё! Вы успешно подписали и опубликовали свои изменения.
## Поддержите проект ## Поддержите проект
- Bitcoin: 1QDhxQ6PraUZa21ET5fYUCPgdrwBomnFgX ### Вы можете стать одним из сопровождающих
- Paypal: https://zeronet.io/docs/help_zeronet/donate/
### Спонсоры Нам нужно больше сопровождающих! Станьте им сегодня! Вам не нужно знать, как кодировать,
есть много другой работы.
* Улучшенная совместимость с MacOS / Safari стала возможной благодаря [BrowserStack.com](https://www.browserstack.com) ### Исправленные баги & новые функции
#### Спасибо! Мы решили пойти дальше и создать идеальную сеть p2p, поэтому нам нужна дополнительная помощь в воплощении этой идеи.
* Больше информации, помощь, журнал изменений, zeronet сайты: https://www.reddit.com/r/zeronet/ ### Создайте свой сайт / переносите свой контент
* Приходите, пообщайтесь с нами: [#zeronet @ FreeNode](https://kiwiirc.com/client/irc.freenode.net/zeronet) или на [gitter](https://gitter.im/HelloZeroNet/ZeroNet)
* Email: hello@zeronet.io (PGP: CB9613AE) Мы знаем, что документации не хватает, но мы делаем все возможное, чтобы поддержать любого
кто хочет переехать. Не стесняйтесь спрашивать.
### Используйте его и делитесь информацией о его существовании
Обязательно расскажите людям, почему вы используете 0net и этот форк в частности! Люди
должны знать об альтернативах.
### Финансовая поддержка сопровождающих
В настоящее время ведущим разработчиком/сопровождающим этого форка является @caryoscelus. Вы можете
посмотреть способы пожертвования на https://caryoscelus.github.io/donate/ (или проверьте
боковую панель, если вы читаете это на github, чтобы узнать больше). По мере роста нашей команды мы
также создаст командные аккаунты на дружественных краудфандинговых платформах.
Если вы хотите, чтобы ваше пожертвование было признано пожертвованием для этого
проекта, для этого также есть специальный биткоин-адрес:
1Kjuw3reZvxRVNs27Gen7jPJYCn6LY7Fg6. Либо если хотите сделать более анонимный донат, вы
можете пожертвовать Monero:
4AiYUcqVRH4C2CVr9zbBdkhRnJnHiJoypHEsq4N7mQziGUoosPCpPeg8SPr87nvwypaRzDgMHEbWWDekKtq8hm9LBmgcMzC
Если вы хотите сделать пожертвование другим способом, не стесняйтесь обращаться к сопровождающему или
создать запрос

View file

@ -1,123 +1,238 @@
# 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/) # zeronet-conservancy
[English](./README.md) [![Packaging status](https://repology.org/badge/vertical-allrepos/zeronet-conservancy.svg)](https://repology.org/project/zeronet-conservancy/versions)
使用 Bitcoin 加密和 BitTorrent 网络的去中心化网络 - https://zeronet.io **(注意:本文档的翻译版本通常滞后于最新内容)**
[in English](README.md) | [em português](README-ptbr.md) | [по-русски](README-ru.md) | [日本語](README-ja.md)
## 为什么? `zeronet-conservancy` 是 [零网 ZeroNet](https://github.com/HelloZeroNet/ZeroNet) 项目的一个分支/延续其创建者已放弃维护致力于维持现有的p2p网络进一步发扬去中心化与自由的价值观并逐步得到一个设计得更好的网络。
* 我们相信开放,自由,无审查的网络和通讯 ## 本分支的状况
* 不会受单点故障影响:只要有在线的节点,站点就会保持在线
* 无托管费用:站点由访问者托管
* 无法关闭:因为节点无处不在
* 快速并可离线运行:即使没有互联网连接也可以使用
在onion-v3切换危机期间我们需要一个能够支持onion-v3且不仅仅依赖于个别开发者的可信的分支。本分支从实现这一任务开始对 [ZeroNet/py3](https://github.com/HelloZeroNet/ZeroNet/tree/py3) 分支作了最少的改动,从而让任何人都可以轻松地审计这些改动。
目前0net正陷入前所未有的危机而本分支似乎是唯一存活下来的。我们的开发进展缓慢但其中的部分工作正在幕后进行。如果你完全是个0net的新手且没有人指导你而且你也不是开发者的话我们建议请等待v0.8版本发布。
## 为何选择0net
* 我们相信开放、自由和不受审查的网络与通讯。
* 不受单点故障影响:只要至少有一个节点在提供服务,站点就能保持在线。
* 无需托管费用:站点由访问者们来共同托管。
* 无法被关停:因为节点无处不在。
* 快速并且支持离线访问:即使没有互联网,你也可以访问站点。
## 功能 ## 功能
* 实时站点更新 * 实时更新站点
* 支持 Namecoin 的 .bit 域名 * 一键克隆已有的站点
* 安装方便:只需解压并运行 * 使用私钥/公钥的免密码认证
* 一键克隆存在的站点 * 内置SQL服务器支持P2P数据同步便于动态站点开发
* 无需密码、基于 [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) * 匿名性:支持采用.onion匿踪服务的Tor网络支持onion-v3
的认证:您的账户被与比特币钱包相同的加密方法保护 * TLS加密连接会经过明网
* 内建 SQL 服务器和 P2P 数据同步:让开发更简单并提升加载速度 * 自动开启uPnP端口可选
* 匿名性:完整的 Tor 网络支持,支持通过 .onion 隐藏服务相互连接而不是通过 IPv4 地址连接 * 支持多用户插件openproxy
* TLS 加密连接 * 兼容任何现代浏览器/操作系统
* 自动打开 uPnP 端口 * 离线站点可通过其他方式进行同步(或当连接恢复时同步)
* 多用户openproxy支持的插件
* 适用于任何浏览器 / 操作系统
## 它是如何工作的?
## 原理 * 启动`zeronet.py`后你可以通过以下方式访问zeronet站点 `http://127.0.0.1:43110/{零网站点地址}` (例如 `http://127.0.0.1:43110/1MCoA8rQHhwu4LY2t2aabqcGSRqrL8uf2X/`
* 当你访问一个新站点时它会通过BitTorrent网络来寻找可用节点从这些节点下载站点所需的文件如html, css, js...)。
* 每个你访问过的站点也由你提供托管服务。
* 每个站点包含一个`content.json`文件其中包含着由本站所有文件生成的sha512哈希值以及由站点私钥生成的签名。
* 如果站点拥有者(即持有站点私钥的人)对站点内容进行了修改,则他/她会重新签署`content.json`文件并发布至其他节点。随后,其他节点会验证`content.json`的完整性(通过签名),并下载被修改的文件,随后将更新过的内容继续发布给其他节点。
* 在运行 `zeronet.py` 后,您将可以通过 以下链接来自原版ZeroNet
`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 加密,站点更新,多用户站点的幻灯片 »](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/)
## 屏幕截图
![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://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/)(已过时)
## 如何加入 ## 如何加入
### Windows ### 从发行版仓库安装
- 下载 [ZeroNet-py3-win64.zip](https://github.com/HelloZeroNet/ZeroNet-win/archive/dist-win64/ZeroNet-py3-win64.zip) (18MB) - NixOS: [搜索zeronet-conservancy软件包](https://search.nixos.org/packages?from=0&size=50&sort=relevance&type=packages&query=zeronet-conservancy)(并参见下方)
- 在任意位置解压缩 - ArchLinux: [最新发行版](https://aur.archlinux.org/packages/zeronet-conservancy)[最新git版](https://aur.archlinux.org/packages/zeronet-conservancy-git)
- 运行 `ZeroNet.exe`
### macOS
- 下载 [ZeroNet-dist-mac.zip](https://github.com/HelloZeroNet/ZeroNet-dist/archive/mac/ZeroNet-dist-mac.zip) (13.2MB) ### 从Nix包管理器安装Linux 或 MacOS
- 在任意位置解压缩
- 运行 `ZeroNet.app`
### Linux (x86-64bit)
- `wget https://github.com/HelloZeroNet/ZeroNet-linux/archive/dist-linux64/ZeroNet-py3-linux64.tar.gz` - 安装并配置nix包管理器如果需要的话
- `tar xvpfz ZeroNet-py3-linux64.tar.gz` - `nix-env -iA nixpkgs.zeronet-conservancy`
- `cd ZeroNet-linux-dist-linux64/`
- 使用以下命令启动 `./ZeroNet.sh` 如果您使用的是NixOS系统将`zeronet-conservancy`添加到系统配置中
- 在浏览器打开 http://127.0.0.1:43110/ 即可访问 ZeroHello 页面
(感谢 @fgaz 制作并维护此软件包)
__提示__ 若要允许在 Web 界面上的远程连接,使用以下命令启动 `./ZeroNet.sh --ui_ip '*' --ui_restrict your.ip.address`
### 从源代码安装 ### 从源代码安装
- `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 页面
## 现有限制 ##### 通用的类Unix系统包括MacOS X
* ~~没有类似于 torrent 的文件拆分来支持大文件~~ (已添加大文件支持) 安装autoconf和其他基本开发工具python3和pip然后看“构建python依赖”
* ~~没有比 BitTorrent 更好的匿名性~~ (已添加内置的完整 Tor 支持)
* 传输文件时没有压缩~~和加密~~ (已添加 TLS 支持) ##### 基于Apt的系统debian、ubuntu等
* 不支持私有站点 - `sudo apt update`
- `sudo apt install git pkg-config libffi-dev python3-pip python3-venv python3-dev build-essential libtool`
##### 基于Red Hat和Fedora的系统
- `yum install epel-release -y 2>/dev/null`
- `yum install git python3 python3-wheel`
##### 基于Fedora系统的dandified包管理器
- `sudo dnf install git python3-pip python3-wheel -y`
##### openSUSE
- `sudo zypper install python3-pip python3-setuptools python3-wheel`
##### 基于Arch和Manjaro的系统
- `sudo pacman -S git python-pip -v --no-confirm`
##### Android/Termux
- 安装 [Termux](https://termux.com/)在Termux中可以通过以下命令安装包`pkg install <package-names>`
- `pkg update`
- `pkg install python automake git binutils libtool`
- 在旧Android系统上可能还需安装这个`pkg install openssl-tool libcrypt clang`
- (若安装了上述包仍在启动时遇到问题,请告诉我们)
- (可选)`pkg install tor`
- 可选通过以下命令运行Tor`tor --ControlPort 9051 --CookieAuthentication 1`(你可以通过右滑来打开新会话)
#### 构建python依赖、虚拟环境、运行
- 克隆此repo注意在Android/Termux上应将其克隆到Termux的“home”文件夹中这是因为虚拟环境无法位于`storage/`
- `python3 -m venv venv`创建python虚拟环境命令末尾的`venv`只是个名称,若使用其他名称,需在后续命令中替换)
- `source venv/bin/activate`(激活环境)
- `python3 -m pip install -r requirements.txt`(安装依赖)
- `python3 zeronet.py`**运行zeronet-conservancy**
- 在浏览器中打开登录页面http://127.0.0.1:43110/
- 需要在新的终端再次启动的话进入repo目录并执行
- `source venv/bin/activate`
- `python3 zeronet.py`
#### 可选用在NixOS上
- 克隆此repo
- 进入已经安装过依赖的shell`nix-shell '<nixpkgs>' -A zeronet-conservancy`
- `./zeronet.py`
#### 可选用构建Docker镜像
- 构建0net镜像`docker build -t 0net-conservancy:latest . -f Dockerfile`
- 或构建集成tor的0net镜像`docker build -t 0net-conservancy:latest . -f Dockerfile.integrated_tor`
- 运行它:`docker run --rm -it -v </path/to/0n/data/directory>:/app/data -p 43110:43110 -p 26552:26552 0net-conservancy:latest`
- /path/to/0n/data/directory - 该目录保存所有数据,包括你的私钥证书。如果在生产模式下运行,请千万不要删除这个文件夹!
- 也可以通过docker-compose运行`docker compose up -d 0net-conservancy` 来启动两个容器——0net和tor各自运行。
- 或者在同一个容器中运行0net和tor`docker compose up -d 0net-tor` 。
(请检查上述说明是否仍然可行)
#### 可选用单行命令(提供者@ssdifnskdjfnsdjk全局安装Python依赖
克隆Github仓库并安装所需的Python模块。首先编辑命令开头的zndir将其改为你想要存储`zeronet-conservancy`的路径:
`zndir="/home/user/myapps/zeronet" ; if [[ ! -d "$zndir" ]]; then git clone --recursive "https://github.com/zeronet-conservancy/zeronet-conservancy.git" "$zndir" && cd "$zndir"||exit; else cd "$zndir";git pull origin master; fi; cd "$zndir" && pip install -r requirements.txt|grep -v "already satisfied"; echo "Try to run: python3 $(pwd)/zeronet.py"`
(此命令也可用于保持`zeronet-conservancy`是最新版本)
#### 可选用脚本
- 安装通用依赖和克隆repo如上所述
运行`start-venv.sh`它将为你创建虚拟环境并安装Python依赖
- 以后会添加更多便捷脚本
### 非官方的Windows系统构建版本
下载并解压以下.zip压缩包
[zeronet-conservancy-0.7.10-unofficial-win64.zip](https://github.com/zeronet-conservancy/zeronet-conservancy/releases/download/v0.7.10/zeronet-conservancy-0.7.10-unofficial-win64.zip)
### 在Windows操作系统下构建
(这些说明正在测试当中,请帮助我们测试并改进它!)
- 安装Python https://www.python.org/downloads/
- 安装在Windows下适用于Python的编译器这对我这种非Windows用户来说是最难的一步详见 https://wiki.python.org/moin/WindowsCompilers ,之后我会提供更多参考)
- [可选,如需获取最新开发版本]安装Githttps://git-scm.com/downloads
- [可选如需为更好的连接性和匿名性来使用tor]安装tor浏览器https://www.torproject.org/download/
- 打开git bash控制台
- 在命令行输入或粘贴`git clone https://github.com/zeronet-conservancy/zeronet-conservancy.git`
- 等待直至git下载好最新的开发版本然后继续在控制台操作
- `cd zeronet-conservancy`
- `python -m venv venv`创建Python虚拟环境
- `venv\Scripts\activate`(激活环境)
- `pip install -r requirements.txt`安装Python依赖有些使用者反馈此命令未能成功安装所有依赖需手动逐个安装依赖
- 注意如果上一步失败了可能说明C/C++编译器未能成功安装)
- [可选,为更好的连接性和匿名性]启动Tor浏览器
- 注意Windows可能会弹窗提示出于“安全原因”阻止了网络访问—请选择允许访问
- `python zeronet.py --tor_proxy 127.0.0.1:9150 --tor_controller 127.0.0.1:9151`启动zeronet-conservancy
- [需要完全匿名,要用如下方式启动]`python zeronet.py --tor_proxy 127.0.0.1:9150 --tor_controller 127.0.0.1:9151 --tor always`
- 在浏览器中访问http://127.0.0.1:43110
构建.exe文件
- 按照上述步骤操作完成后,再执行
- `pip install pyinstaller`
- `pyinstaller -p src -p plugins --hidden-import merkletools --hidden-import lib.bencode_open --hidden-import Crypt.Crypt --hidden-import Db.DbQuery --hidden-import lib.subtl --hidden-import lib.subtl.subtl --hidden-import sockshandler --add-data "src;src" --add-data "plugins;plugins" --clean zeronet.py`
- dist/zeronet 路径下会包含可用的 zeronet.exe
## 目前的局限性
* 文件传输未压缩
* 不支持私密站点
* 不支持DHT
* 不支持I2P
* 不支持中心化的模块比如zeroid我们正在改进
* 尚无可靠的垃圾信息保护措施(我们也在改进)
* 无法直接在浏览器中使用(中长期会优先解决此问题)
* 不支持数据透明
* 尚无可重现构建
* 不支持磁盘加密存储
* 尚无可重现构建因此在部分GNU/Linux发行版之外无法构建
## 如何创建一个 ZeroNet 站点? ## 我该如何创建一个ZeroNet站点
* 点击 [ZeroHello](http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D) 站点的 **⋮** > **「新建空站点」** 菜单项 * 点击[仪表盘](http://127.0.0.1:43110/191CazMVNaAcT9Y1zhkxd9ixMBPs59g2um/)菜单中的 **⋮** > **"Create new, empty site"** 。
* 您将被**重定向**到一个全新的站点,该站点只能由您修改 * 你将被**重定向**到一个只有你可以修改的全新站点!
* 您可以在 **data/[您的站点地址]** 目录中找到并修改网站的内容 * 你可以在 **data/[你的站点地址]** 目录中找到并修改你的站点内容。
* 修改后打开您的网站将右上角的「0」按钮拖到左侧然后点击底部的**签名**并**发布**按钮 * 修改完成后打开你的站点向左拖动页面右上角的“0”按钮然后点击底部的**sign and publish**按钮。
接下来的步骤:[ZeroNet 开发者文档](https://zeronet.io/docs/site_development/getting_started/) 下一步请看:[ZeroNet开发者文档](https://zeronet.io/docs/site_development/getting_started/)
## 帮助这个项目 ## 帮助此项目持续发展
- Bitcoin: 1QDhxQ6PraUZa21ET5fYUCPgdrwBomnFgX ### 成为维护者
- Paypal: https://zeronet.io/docs/help_zeronet/donate/
### 赞助商 我们需要更多的维护者!今天就加入吧!你不需要懂编程,这里还有很多其他工作要做。
* [BrowserStack.com](https://www.browserstack.com) 使更好的 macOS/Safari 兼容性成为可能 ### 为你的平台创建构建包
#### 感谢您! 我们需要为各个主流操作系统平台提供独立构建的版本并存入各种FLOSS仓库。如果你正在使用的Linux发行版还没有得到我们提供的相应构建包为何不自己创建一个或者如果你不知道怎么做去询问你的系统维护者呢
* 更多信息,帮助,变更记录和 zeronet 站点https://www.reddit.com/r/zeronet/ ### 修复bug & 添加功能
* 前往 [#zeronet @ FreeNode](https://kiwiirc.com/client/irc.freenode.net/zeronet) 或 [gitter](https://gitter.im/HelloZeroNet/ZeroNet) 和我们聊天
* [这里](https://gitter.im/ZeroNet-zh/Lobby)是一个 gitter 上的中文聊天室 我们决心继续前进打造一个完美的p2p网络因此我们需要更多的帮助来实现它。
* Email: hello@zeronet.io (PGP: [960F FF2D 6C14 5AA6 13E8 491B 5B63 BAE6 CB96 13AE](https://zeronet.io/files/tamas@zeronet.io_pub.asc))
### 创建你的站点/带来你的内容
我们知道这份文档有所欠缺,但我们尽力支持任何希望迁移站点和内容的用户。请不要犹豫,随时联系我们。
### 使用并传播
告诉大家你为何选择使用0net及我们这个特别的分支人们需要知道他们的替代方案。
### 资金支持我们
此分支由@caryoscelus创建并维护。你可以在 https://caryoscelus.github.io/donate/ 上查看捐赠方式如果在github上阅读也可查看侧边栏了解更多途径。随着我们团队的不断壮大我们也会在更友善的众筹平台上创建团队账户。
如果你想确保你的捐赠会被视作对本项目的支持,下面是一个专门的比特币地址:
1Kjuw3reZvxRVNs27Gen7jPJYCn6LY7Fg6。如需更匿名和私密的方式以下是Monero钱包地址
4AiYUcqVRH4C2CVr9zbBdkhRnJnHiJoypHEsq4N7mQziGUoosPCpPeg8SPr87nvwypaRzDgMHEbWWDekKtq8hm9LBmgcMzC
如果你希望通过其他方式捐赠请随时联系维护者或创建一个issue。
# 我们在抗议的情况下使用GitHub
该项目当前托管在GitHub。但这并不理想GitHub是一个专有的、有其商业秘密的系统不是自由及开放源代码软件FLOSS。我们对使用像GitHub这样的专有系统来开发我们的FLOSS项目深感担忧。我们有一个[开放的议题页面](https://github.com/zeronet-conservancy/zeronet-conservancy/issues/89)来追踪从长远来看迁移出GitHub的进展。我们强烈建议你阅读[放弃 GitHub](https://GiveUpGitHub.org)运动以及[软件自由保护协会](https://sfconservancy.org)对其的阐述以便了解为什么GitHub不适合托管FOSS项目。
如果你是已经停止使用GitHub的贡献者欢迎[在notabug上查看我们的镜像](https://notabug.org/caryoscelus/zeronet-conservancy),并在那里进行开发,或者通过[联系方式](https://caryoscelus.github.io/contacts/)直接向项目维护者发送git补丁。
无论过去还是现在未经许可地通过GitHub Copilot使用本项目的代码的行为必须立即停止。我们不允许GitHub将本项目代码用于Copilot。
![GiveUpGitHub运动的标志](https://sfconservancy.org/img/GiveUpGitHub.png)

303
README.md
View file

@ -1,11 +1,30 @@
# 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/) ![tests](https://github.com/HelloZeroNet/ZeroNet/workflows/tests/badge.svg) [![Docker Pulls](https://img.shields.io/docker/pulls/nofish/zeronet)](https://hub.docker.com/r/nofish/zeronet) # zeronet-conservancy
Decentralized websites using Bitcoin crypto and the BitTorrent network - https://zeronet.io / [onion](http://zeronet34m3r5ngdu54uj57dcafpgdjhxsgq5kla5con4qvcmfzpvhad.onion) [![Packaging status](https://repology.org/badge/vertical-allrepos/zeronet-conservancy.svg)](https://repology.org/project/zeronet-conservancy/versions)
(NOTE THAT TRANSLATIONS ARE USUALLY BEHIND THIS FILE)
## Why? [по-русски](README-ru.md) | [em português](README-ptbr.md) | [简体中文](README-zh-cn.md) | [日本語](README-ja.md)
* We believe in open, free, and uncensored network and communication. `zeronet-conservancy` is a fork/continuation of [ZeroNet](https://github.com/HelloZeroNet/ZeroNet) project
(that has been abandoned by its creator) that is dedicated to sustaining existing p2p network and developing
its values of decentralization and freedom, while gradually switching to a better designed network
## State of the fork
During onion-v3 switch crisis, we needed a fork that worked with onion-v3 and didn't depend on trust to one or
two people. This fork started from fulfilling that mission, implementing minimal changes to
[ZeroNet/py3](https://github.com/HelloZeroNet/ZeroNet/tree/py3) branch which are easy to audit by anyone.
Now 0net is in deeper crisis than ever before and this fork seems to
be the last one standing. Development is sparse and slow, but some of
the work is being done behind the scenes. If you're completely new to
0net, don't have anybody to guide you there and are not a developer,
we recommend waiting until v0.8 is out.
## Why 0net?
* We believe in open, free, and uncensored networks and communication.
* No single point of failure: Site remains online so long as at least 1 peer is * No single point of failure: Site remains online so long as at least 1 peer is
serving it. serving it.
* No hosting costs: Sites are served by visitors. * No hosting costs: Sites are served by visitors.
@ -16,24 +35,22 @@ Decentralized websites using Bitcoin crypto and the BitTorrent network - https:/
## Features ## Features
* Real-time updated sites * Real-time updated sites
* Namecoin .bit domains support
* Easy to setup: unpack & run
* Clone websites in one click * Clone websites in one click
* Password-less [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) * Password-less authorization using private/public keys
based authorization: Your account is protected by the same cryptography as your Bitcoin wallet * Built-in SQL server with P2P data synchronization: allows easier dynamic site development
* Built-in SQL server with P2P data synchronization: Allows easier site development and faster page load times * Anonymity: Tor network support with .onion hidden services (including onion-v3 support)
* Anonymity: Full Tor network support with .onion hidden services instead of IPv4 addresses * TLS encrypted connections (through clearnet)
* TLS encrypted connections * Automatic uPnP port opening (if opted in)
* Automatic uPnP port opening
* Plugin for multiuser (openproxy) support * Plugin for multiuser (openproxy) support
* Works with any browser/OS * Works with any modern browser/OS
* Works offline and can be synced via alternative transports (or when connection is back)
## How does it work? ## How does it work?
* After starting `zeronet.py` you will be able to visit zeronet sites using * After starting `zeronet.py` you will be able to visit zeronet sites using
`http://127.0.0.1:43110/{zeronet_address}` (eg. `http://127.0.0.1:43110/{zeronet_address}` (eg.
`http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D`). `http://127.0.0.1:43110/1MCoA8rQHhwu4LY2t2aabqcGSRqrL8uf2X/`).
* When you visit a new zeronet site, it tries to find peers using the BitTorrent * When you visit a new zeronet site, it tries to find peers using the BitTorrent
network so it can download the site files (html, css, js...) from them. network so it can download the site files (html, css, js...) from them.
* Each visited site is also served by you. * Each visited site is also served by you.
@ -45,93 +62,225 @@ Decentralized websites using Bitcoin crypto and the BitTorrent network - https:/
signature), they download the modified files and publish the new content to signature), they download the modified files and publish the new content to
other peers. other peers.
#### [Slideshow about ZeroNet cryptography, site updates, multi-user sites »](https://docs.google.com/presentation/d/1_2qK1IuOKJ51pgBvllZ9Yu7Au2l551t3XBgyTSvilew/pub?start=false&loop=false&delayms=3000) Following links relate to original ZeroNet:
#### [Frequently asked questions »](https://zeronet.io/docs/faq/)
#### [ZeroNet Developer Documentation »](https://zeronet.io/docs/site_development/getting_started/)
## Screenshots
![Screenshot](https://i.imgur.com/H60OAHY.png)
![ZeroTalk](https://zeronet.io/docs/img/zerotalk.png)
#### [More screenshots in ZeroNet docs »](https://zeronet.io/docs/using_zeronet/sample_sites/)
- [Slideshow about ZeroNet cryptography, site updates, multi-user sites »](https://docs.google.com/presentation/d/1_2qK1IuOKJ51pgBvllZ9Yu7Au2l551t3XBgyTSvilew/pub?start=false&loop=false&delayms=3000)
- [Frequently asked questions »](https://zeronet.io/docs/faq/)
- [ZeroNet Developer Documentation »](https://zeronet.io/docs/site_development/getting_started/) (getting outdated)
## How to join ## How to join
### Windows ### Install from your distribution repository
- Download [ZeroNet-py3-win64.zip](https://github.com/HelloZeroNet/ZeroNet-win/archive/dist-win64/ZeroNet-py3-win64.zip) (18MB) - NixOS: [zeronet-conservancy packages search](https://search.nixos.org/packages?from=0&size=50&sort=relevance&type=packages&query=zeronet-conservancy) (and see below)
- Unpack anywhere - ArchLinux: [latest release](https://aur.archlinux.org/packages/zeronet-conservancy), [fresh git version](https://aur.archlinux.org/packages/zeronet-conservancy-git)
- Run `ZeroNet.exe`
### macOS
- Download [ZeroNet-dist-mac.zip](https://github.com/HelloZeroNet/ZeroNet-dist/archive/mac/ZeroNet-dist-mac.zip) (13.2MB) ### Install from Nix package manager (Linux or MacOS)
- Unpack anywhere
- Run `ZeroNet.app` - install & configure nix package manager (if needed)
- `nix-env -iA nixpkgs.zeronet-conservancy`
### Linux (x86-64bit)
- `wget https://github.com/HelloZeroNet/ZeroNet-linux/archive/dist-linux64/ZeroNet-py3-linux64.tar.gz` or add `zeronet-conservancy` to your system configuration if you're on NixOS
- `tar xvpfz ZeroNet-py3-linux64.tar.gz`
- `cd ZeroNet-linux-dist-linux64/` (thanks @fgaz for making & maintaining the package)
- Start with: `./ZeroNet.sh`
- Open the ZeroHello landing page in your browser by navigating to: http://127.0.0.1:43110/
__Tip:__ Start with `./ZeroNet.sh --ui_ip '*' --ui_restrict your.ip.address` to allow remote connections on the web interface.
### Android (arm, arm64, x86)
- minimum Android version supported 16 (JellyBean)
- [<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
alt="Download from Google Play"
height="80">](https://play.google.com/store/apps/details?id=in.canews.zeronetmobile)
- APK download: https://github.com/canewsin/zeronet_mobile/releases
- XDA Labs: https://labs.xda-developers.com/store/app/in.canews.zeronet
#### Docker
There is an official image, built from source at: https://hub.docker.com/r/nofish/zeronet/
### Install from source ### Install from source
- `wget https://github.com/HelloZeroNet/ZeroNet/archive/py3/ZeroNet-py3.tar.gz` #### System dependencies
- `tar xvpfz ZeroNet-py3.tar.gz`
- `cd ZeroNet-py3` ##### Generic unix-like (including mac os x)
- `sudo apt-get update`
- `sudo apt-get install python3-pip` Install autoconf and other basic development tools, python3 and pip, then proceed to "building python dependencies"
- `sudo python3 -m pip install -r requirements.txt` (if running fails due to missing dependency, please report it/make pull request to fix dependency list)
- Start with: `python3 zeronet.py`
- Open the ZeroHello landing page in your browser by navigating to: http://127.0.0.1:43110/ ##### Apt-based (debian, ubuntu, etc)
- `sudo apt update`
- `sudo apt install git pkg-config libffi-dev python3-pip python3-venv python3-dev build-essential libtool`
##### Red Hat and Fedora based
- `yum install epel-release -y 2>/dev/null`
- `yum install git python3 python3-wheel`
##### Fedora based dandified
- `sudo dnf install git python3-pip python3-wheel -y`
##### openSUSE
- `sudo zypper install python3-pip python3-setuptools python3-wheel`
##### Arch and Manjaro based
- `sudo pacman -S git python-pip -v --no-confirm`
##### Android/Termux
- install [Termux](https://termux.com/) (in Termux you can install packages via `pkg install <package-names>`)
- `pkg update`
- `pkg install python automake git binutils libtool`
- (on an older android versions you may also need to install) `pkg install openssl-tool libcrypt clang`
- (if you've installed the above packages and still run into launch issues, please report)
- (optional) `pkg install tor`
- (optional) run tor via `tor --ControlPort 9051 --CookieAuthentication 1` command (you can then open new session by swiping to the right)
#### Building python dependencies, venv & running
- clone this repo (NOTE: on Android/Termux you should clone it into "home" folder of Termux, because virtual environment cannot live in `storage/`)
- `python3 -m venv venv` (make python virtual environment, the last `venv` is just a name, if you use different you should replace it in later commands)
- `source venv/bin/activate` (activate environment)
- `python3 -m pip install -r requirements.txt` (install dependencies)
- `python3 zeronet.py` (**run zeronet-conservancy!**)
- open the landing page in your browser by navigating to: http://127.0.0.1:43110/
- to start it again from fresh terminal, you need to navigate to repo directory and:
- `source venv/bin/activate`
- `python3 zeronet.py`
#### (alternatively) On NixOS
- clone this repo
- `nix-shell '<nixpkgs>' -A zeronet-conservancy` to enter shell with installed dependencies
- `./zeronet.py`
#### (alternatively) Build Docker image
- build 0net image: `docker build -t 0net-conservancy:latest . -f Dockerfile`
- or build 0net image with integrated tor: `docker build -t 0net-conservancy:latest . -f Dockerfile.integrated_tor`
- and run it: `docker run --rm -it -v </path/to/0n/data/directory>:/app/data -p 43110:43110 -p 26552:26552 0net-conservancy:latest`
- /path/to/0n/data/directory - directory, where all data will be saved, including your secret certificates. If you run it with production mode, do not remove this folder!
- or you can run it with docker-compose: `docker compose up -d 0net-conservancy` up two containers - 0net and tor separately.
- or: `docker compose up -d 0net-tor` for run 0net and tor in one container.
(please check if these instructions are still accurate)
#### Alternative one-liner (by @ssdifnskdjfnsdjk) (installing python dependencies globally)
Clone Github repository and install required Python modules. First
edit zndir path at the begining of the command, to be the path where
you want to store `zeronet-conservancy`:
`zndir="/home/user/myapps/zeronet" ; if [[ ! -d "$zndir" ]]; then git clone --recursive "https://github.com/zeronet-conservancy/zeronet-conservancy.git" "$zndir" && cd "$zndir"||exit; else cd "$zndir";git pull origin master; fi; cd "$zndir" && pip install -r requirements.txt|grep -v "already satisfied"; echo "Try to run: python3 $(pwd)/zeronet.py"`
(This command can also be used to keep `zeronet-conservancy` up to date)
#### Alternative script
- after installing general dependencies and cloning repo (as above),
run `start-venv.sh` which will create a virtual env for you and
install python requirements
- more convenience scripts to be added soon
### (unofficial) Windows OS build
Download and extract .zip archive
[zeronet-conservancy-0.7.10-unofficial-win64.zip](https://github.com/zeronet-conservancy/zeronet-conservancy/releases/download/v0.7.10/zeronet-conservancy-0.7.10-unofficial-win64.zip)
### Building under Windows OS
(These instructions are work-in-progress, please help us test it and improve it!)
- install python from https://www.python.org/downloads/
- install some windows compiler suitable for python , this proved to be the most difficult part for me as non-windows user (see here https://wiki.python.org/moin/WindowsCompilers and i'll link more references later)
- [optionally to get latest dev version] install git from https://git-scm.com/downloads
- [optionally to use tor for better connectivity and anonymization] install tor browser from https://www.torproject.org/download/
- open git bash console
- type/copypaste `git clone https://github.com/zeronet-conservancy/zeronet-conservancy.git` into command line
- wait till git downloads latest dev version and continue in console
- `cd zeronet-conservancy`
- `python -m venv venv` (create virtual python environment)
- `venv\Scripts\activate` (this activates the environment)
- `pip install -r requirements.txt` (install python dependencies) (some users reported that this command doesn't successfully install requirements and only manual installation of dependencies one by one works)
- (NOTE: if previous step fails, it most likely means you haven't installed c/c++ compiler successfully)
- [optional for tor for better connectivity and anonymity] launch Tor Browser
- (NOTE: windows might show a window saying it blocked access to internet for "security reasons" — you should allow the access)
- `python zeronet.py --tor_proxy 127.0.0.1:9150 --tor_controller 127.0.0.1:9151` (launch zeronet-conservancy!)
- [for full tor anonymity launch this instead] `python zeronet.py --tor_proxy 127.0.0.1:9150 --tor_controller 127.0.0.1:9151 --tor always`
- navigate to http://127.0.0.1:43110 in your favourite browser!
To build .exe
- follow the same steps as above, but additionally
- `pip install pyinstaller`
- `pyinstaller -p src -p plugins --hidden-import merkletools --hidden-import lib.bencode_open --hidden-import Crypt.Crypt --hidden-import Db.DbQuery --hidden-import lib.subtl --hidden-import lib.subtl.subtl --hidden-import sockshandler --add-data "src;src" --add-data "plugins;plugins" --clean zeronet.py`
- dist/zeronet should contain working zeronet.exe!
## Current limitations ## Current limitations
* ~~No torrent-like file splitting for big file support~~ (big file support added) * File transactions are not compressed
* ~~No more anonymous than Bittorrent~~ (built-in full Tor support added)
* File transactions are not compressed ~~or encrypted yet~~ (TLS encryption added)
* No private sites * No private sites
* No DHT support
* No I2P support
* Centralized elements like zeroid (we're working on this!)
* No reliable spam protection (and on this too)
* Doesn't work directly from browser (one of the top priorities for mid-future)
* No data transparency
* No reproducible builds
* No on-disk encryption
* No reproducible builds (hence no builds beyond certain GNU/Linux distributions)
## How can I create a ZeroNet site? ## How can I create a ZeroNet site?
* Click on **⋮** > **"Create new, empty site"** menu item on the site [ZeroHello](http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D). * Click on **⋮** > **"Create new, empty site"** menu item on the [dashboard](http://127.0.0.1:43110/191CazMVNaAcT9Y1zhkxd9ixMBPs59g2um/).
* You will be **redirected** to a completely new site that is only modifiable by you! * 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 * 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 * After the modifications open your site, drag the topright "0" button to the left, then press **sign and publish** button on the bottom
Next steps: [ZeroNet Developer Documentation](https://zeronet.io/docs/site_development/getting_started/) Next steps: [ZeroNet Developer Documentation](https://zeronet.io/docs/site_development/getting_started/)
## Help keep this project alive ## Help this project stay alive
- Bitcoin: 1QDhxQ6PraUZa21ET5fYUCPgdrwBomnFgX ### Become a maintainer
- Paypal: https://zeronet.io/docs/help_zeronet/donate/
### Sponsors We need more maintainers! Become one today! You don't need to know how to code,
there's a lot of other work to do.
* Better macOS/Safari compatibility made possible by [BrowserStack.com](https://www.browserstack.com) ### Make builds for your platforms
#### Thank you! We need reproducible stand-alone builds for major platforms, as well as presense in various FLOSS
repositories. If you're using one of Linux distributions which don't have packages yet, why not make
a package for it or (if you don't know how) ask a maintainer now?
* More info, help, changelog, zeronet sites: https://www.reddit.com/r/zeronet/ ### Fix bugs & add features
* 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: [960F FF2D 6C14 5AA6 13E8 491B 5B63 BAE6 CB96 13AE](https://zeronet.io/files/tamas@zeronet.io_pub.asc)) We've decided to go ahead and make a perfect p2p web, so we need more help
implementing it.
### Make your site/bring your content
We know the documentation is lacking, but we try our best to support anyone
who wants to migrate. Don't hesitate to ask.
### Use it and spread the word
Make sure to tell people why do you use 0net and this fork in particular! People
need to know their alternatives.
### Financially support maintainers
This fork was created and maintained by @caryoscelus. You can
see ways to donate to them on https://caryoscelus.github.io/donate/ (or check
sidebar if you're reading this on github for more ways). As our team grows, we
will create team accounts on friendly crowdfunding platforms as well.
If you want to make sure your donation is recognized as donation for this
project, there is a dedicated bitcoin address for that, too:
1Kjuw3reZvxRVNs27Gen7jPJYCn6LY7Fg6. And if you want to stay more anonymous and
private, a Monero wallet:
4AiYUcqVRH4C2CVr9zbBdkhRnJnHiJoypHEsq4N7mQziGUoosPCpPeg8SPr87nvwypaRzDgMHEbWWDekKtq8hm9LBmgcMzC
If you want to donate in a different way, feel free to contact maintainer or
create an issue
# We're using GitHub under protest
This project is currently hosted on GitHub. This is not ideal; GitHub is a
proprietary, trade-secret system that is not Free/Libre and Open Souce Software
(FLOSS). We are deeply concerned about using a proprietary system like GitHub
to develop our FLOSS project. We have an
[open issue](https://github.com/zeronet-conservancy/zeronet-conservancy/issues/89)
to track moving away from GitHub in the long term. We urge you to read about the
[Give up GitHub](https://GiveUpGitHub.org) campaign from
[the Software Freedom Conservancy](https://sfconservancy.org) to understand
some of the reasons why GitHub is not a good place to host FOSS projects.
If you are a contributor who personally has already quit using GitHub, feel
free to [check out from our mirror on notabug](https://notabug.org/caryoscelus/zeronet-conservancy)
and develop there or send git patches directly to project maintainer via
preffered [contact channel](https://caryoscelus.github.io/contacts/).
Any use of this project's code by GitHub Copilot, past or present, is done
without our permission. We do not consent to GitHub's use of this project's
code in Copilot.
![Logo of the GiveUpGitHub campaign](https://sfconservancy.org/img/GiveUpGitHub.png)

45
Vagrantfile vendored
View file

@ -1,45 +0,0 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
#Set box
config.vm.box = "ubuntu/trusty64"
#Do not check fo updates
config.vm.box_check_update = false
#Add private network
config.vm.network "private_network", type: "dhcp"
#Redirect ports
config.vm.network "forwarded_port", guest: 43110, host: 43110
config.vm.network "forwarded_port", guest: 15441, host: 15441
#Sync folder using NFS if not windows
config.vm.synced_folder ".", "/vagrant",
:nfs => !Vagrant::Util::Platform.windows?
#Virtal Box settings
config.vm.provider "virtualbox" do |vb|
# Don't boot with headless mode
#vb.gui = true
# Set VM settings
vb.customize ["modifyvm", :id, "--memory", "512"]
vb.customize ["modifyvm", :id, "--cpus", 1]
end
#Update system
config.vm.provision "shell",
inline: "sudo apt-get update -y && sudo apt-get upgrade -y"
#Install deps
config.vm.provision "shell",
inline: "sudo apt-get install msgpack-python python-gevent python-pip python-dev -y"
config.vm.provision "shell",
inline: "sudo pip install msgpack --upgrade"
end

1
bootstrap.url Normal file
View file

@ -0,0 +1 @@
https://github.com/zeronet-conservancy/zeronet-conservancy/releases/download/v0.7.10/data-default-2023-09-03.zip

55
build.py Executable file
View file

@ -0,0 +1,55 @@
#!/usr/bin/env python3
## Copyright (c) 2024 caryoscelus
##
## zeronet-conservancy is free software: you can redistribute it and/or modify it under the
## terms of the GNU General Public License as published by the Free Software
## Foundation, either version 3 of the License, or (at your option) any later version.
##
## zeronet-conservancy is distributed in the hope that it will be useful, but
## WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
## details.
##
## You should have received a copy of the GNU General Public License along with
## zeronet-conservancy. If not, see <https://www.gnu.org/licenses/>.
##
"""Simple build/bundle script
"""
import argparse
def write_to(args, target):
branch = args.branch
commit = args.commit
if branch is None or commit is None:
from src.util import Git
branch = branch or Git.branch() or 'unknown'
commit = commit or Git.commit() or 'unknown'
target.write('\n'.join([
f"build_type = {args.type!r}",
f"branch = {branch!r}",
f"commit = {commit!r}",
f"version = {args.version!r}",
f"platform = {args.platform!r}",
]))
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--type', default='source')
parser.add_argument('--version')
parser.add_argument('--branch')
parser.add_argument('--commit')
parser.add_argument('--platform', default='source')
parser.add_argument('--stdout', action=argparse.BooleanOptionalAction, default=False)
args = parser.parse_args()
if args.stdout:
import sys
target = sys.stdout
else:
target = open('src/Build.py', 'w')
write_to(args, target)
if __name__ == '__main__':
main()

25
docker/Dockerfile Normal file
View file

@ -0,0 +1,25 @@
FROM python:3.12-alpine
RUN apk --update --no-cache --no-progress add git gcc libffi-dev musl-dev make openssl g++ autoconf automake libtool
RUN adduser -u 1600 -D service-0net
USER service-0net:service-0net
WORKDIR /home/service-0net
COPY requirements.txt .
RUN python3 -m pip install -r requirements.txt
# the part below is updated with source updates
COPY . .
ENTRYPOINT python3 zeronet.py --ui_ip "*" --fileserver_port 26552 \
--tor $TOR_ENABLED --tor_controller tor:$TOR_CONTROL_PORT \
--tor_proxy tor:$TOR_SOCKS_PORT --tor_password $TOR_CONTROL_PASSWD
CMD main
EXPOSE 43110 26552

26
docker/debian.Dockerfile Normal file
View file

@ -0,0 +1,26 @@
FROM python:3.12-slim-bookworm
RUN apt-get update
RUN apt-get -y install git openssl pkg-config libffi-dev python3-pip python3-dev build-essential libtool
RUN useradd -u 1600 -m service-0net
USER service-0net:service-0net
WORKDIR /home/service-0net
COPY requirements.txt .
RUN python3 -m pip install -r requirements.txt
# the part below is updated with source updates
COPY . .
ENTRYPOINT python3 zeronet.py --ui_ip "*" --fileserver_port 26552 \
--tor $TOR_ENABLED --tor_controller tor:$TOR_CONTROL_PORT \
--tor_proxy tor:$TOR_SOCKS_PORT --tor_password $TOR_CONTROL_PASSWD
CMD main
EXPOSE 43110 26552

55
docker/docker-compose.yml Normal file
View file

@ -0,0 +1,55 @@
version: '3'
services:
tor:
tty: true
stdin_open: true
build:
context: ..
dockerfile: docker/tor.Dockerfile
networks:
- 0net-network
environment: &tor-environments
# since we are using tor internally, password doesn't really matter
TOR_CONTROL_PASSWD: some_password
TOR_SOCKS_PORT: 9050
TOR_CONTROL_PORT: 9051
0net-conservancy:
tty: true
stdin_open: true
build:
context: ..
dockerfile: docker/Dockerfile
networks:
- 0net-network
volumes:
# NOTE: this refers to docker/data..
- ./data:/home/service-0net/data
ports:
- "26552:26552"
- "127.0.0.1:43110:43110"
depends_on:
- tor
environment:
TOR_ENABLED: enable
<<: *tor-environments
# integrated container with tor
0net-tor:
tty: true
stdin_open: true
build:
context: ..
dockerfile: docker/znctor.Dockerfile
networks:
- 0net-network
volumes:
# NOTE: this refers to docker/data..
- ./data:/home/service-0net/data
ports:
- "26552:26552"
- "127.0.0.1:43110:43110"
networks:
0net-network:

9
docker/tor.Dockerfile Normal file
View file

@ -0,0 +1,9 @@
FROM alpine:3.18
RUN apk --update --no-cache --no-progress add tor
USER tor
CMD tor --SocksPort 0.0.0.0:${TOR_SOCKS_PORT} --ControlPort 0.0.0.0:${TOR_CONTROL_PORT} --HashedControlPassword $(tor --quiet --hash-password $TOR_CONTROL_PASSWD)
EXPOSE $TOR_SOCKS_PORT $TOR_CONTROL_PORT

31
docker/znctor.Dockerfile Normal file
View file

@ -0,0 +1,31 @@
FROM python:3.12-alpine
RUN apk --update --no-cache --no-progress add git gcc libffi-dev musl-dev make openssl g++ autoconf automake libtool
RUN apk add tor
RUN echo "ControlPort 9051" >> /etc/tor/torrc
RUN echo "CookieAuthentication 1" >> /etc/tor/torrc
RUN adduser -u 1600 -D service-0net
USER service-0net:service-0net
WORKDIR /home/service-0net
COPY requirements.txt .
RUN python3 -m pip install -r requirements.txt
RUN echo "tor &" > start.sh
RUN echo "python3 zeronet.py --ui_ip '*' --fileserver_port 26552" >> start.sh
RUN chmod +x start.sh
# the part below is updated with source updates
COPY . .
ENTRYPOINT ./start.sh
CMD main
EXPOSE 43110 26552

35
greet.py Normal file
View file

@ -0,0 +1,35 @@
def grad(n):
s = 0x08
r = 0xff
g = 0x00
b = 0x00
for i in range(n):
if r >= s and b < s:
r -= s
g += s
elif g >= s and r < s:
g -= s
b += s
elif b >= s and g < s:
b -= s
r += s
return f'#{r:02x}{g:02x}{b:02x}'
def fancy_greet(version):
from rich.console import Console
from rich.text import Text
zc_msg = fr'''
||| . . _ _._|_ _. . . _ .__ _.. _. . __.. _ __. .
||| //\|/ |/_| | == / / \|/ |( /_||/ | | __||/ |/ \_|
||| \_/| |\_ |. \__\_/| |_) \_ | \/ |__|| |\__ _/
|||
||| v{version}
'''
lns = zc_msg.split('\n')
console = Console()
for l in lns:
txt = Text(l)
txt.stylize('bold')
for i in range(len(l)):
txt.stylize(grad(i), i, i+1)
console.print(txt)

View file

@ -31,7 +31,7 @@ class LocalAnnouncer(BroadcastServer.BroadcastServer):
self.sender_info["peer_id"] = self.server.peer_id self.sender_info["peer_id"] = self.server.peer_id
self.sender_info["port"] = self.server.port self.sender_info["port"] = self.server.port
self.sender_info["broadcast_port"] = listen_port self.sender_info["broadcast_port"] = listen_port
self.sender_info["rev"] = config.rev self.sender_info["rev"] = config.user_agent_rev
self.known_peers = {} self.known_peers = {}
self.last_discover = 0 self.last_discover = 0
@ -142,6 +142,6 @@ class FileServerPlugin(object):
class ConfigPlugin(object): class ConfigPlugin(object):
def createArguments(self): def createArguments(self):
group = self.parser.add_argument_group("AnnounceLocal plugin") group = self.parser.add_argument_group("AnnounceLocal plugin")
group.add_argument('--broadcast_port', help='UDP broadcasting port for local peer discovery', default=1544, type=int, metavar='port') group.add_argument('--broadcast-port', help='UDP broadcasting port for local peer discovery', default=1544, type=int, metavar='port')
return super(ConfigPlugin, self).createArguments() return super(ConfigPlugin, self).createArguments()

View file

@ -14,7 +14,7 @@ from util import helper
class TrackerStorage(object): class TrackerStorage(object):
def __init__(self): def __init__(self):
self.log = logging.getLogger("TrackerStorage") self.log = logging.getLogger("TrackerStorage")
self.file_path = "%s/trackers.json" % config.data_dir self.file_path = config.start_dir / 'trackers.json'
self.load() self.load()
self.time_discover = 0.0 self.time_discover = 0.0
atexit.register(self.save) atexit.register(self.save)
@ -185,6 +185,6 @@ class FileServerPlugin(object):
class ConfigPlugin(object): class ConfigPlugin(object):
def createArguments(self): def createArguments(self):
group = self.parser.add_argument_group("AnnounceShare plugin") group = self.parser.add_argument_group("AnnounceShare plugin")
group.add_argument('--working_shared_trackers_limit', help='Stop discovering new shared trackers after this number of shared trackers reached', default=5, type=int, metavar='limit') group.add_argument('--working-shared-trackers-limit', help='Stop discovering new shared trackers after this number of shared trackers reached', default=5, type=int, metavar='limit')
return super(ConfigPlugin, self).createArguments() return super(ConfigPlugin, self).createArguments()

View file

@ -9,7 +9,7 @@ from Config import config
@pytest.mark.usefixtures("resetTempSettings") @pytest.mark.usefixtures("resetTempSettings")
class TestAnnounceShare: class TestAnnounceShare:
def testAnnounceList(self, file_server): def testAnnounceList(self, file_server):
open("%s/trackers.json" % config.data_dir, "w").write("{}") (config.start_dir / 'trackers.json').open('w').write('{}')
tracker_storage = AnnounceSharePlugin.tracker_storage tracker_storage = AnnounceSharePlugin.tracker_storage
tracker_storage.load() tracker_storage.load()
peer = Peer(file_server.ip, 1544, connection_server=file_server) peer = Peer(file_server.ip, 1544, connection_server=file_server)

View file

@ -124,7 +124,7 @@ class SiteAnnouncerPlugin(object):
sign = CryptRsa.sign(res["onion_sign_this"].encode("utf8"), self.site.connection_server.tor_manager.getPrivatekey(onion)) sign = CryptRsa.sign(res["onion_sign_this"].encode("utf8"), self.site.connection_server.tor_manager.getPrivatekey(onion))
request["onion_signs"][publickey] = sign request["onion_signs"][publickey] = sign
res = tracker_peer.request("announce", request) res = tracker_peer.request("announce", request)
if not res or "onion_sign_this" in res: if not res:
if full_announce: if full_announce:
time_full_announced[tracker_address] = 0 time_full_announced[tracker_address] = 0
raise AnnounceError("Announce onion address to failed: %s" % res) raise AnnounceError("Announce onion address to failed: %s" % res)

View file

@ -413,7 +413,7 @@ class ConfigPlugin(object):
back = super(ConfigPlugin, self).createArguments() back = super(ConfigPlugin, self).createArguments()
if self.getCmdlineValue("test") == "benchmark": if self.getCmdlineValue("test") == "benchmark":
self.test_parser.add_argument( self.test_parser.add_argument(
'--num_multipler', help='Benchmark run time multipler', '--num-multipler', help='Benchmark run time multipler',
default=1.0, type=float, metavar='num' default=1.0, type=float, metavar='num'
) )
self.test_parser.add_argument( self.test_parser.add_argument(
@ -422,7 +422,7 @@ class ConfigPlugin(object):
) )
elif self.getCmdlineValue("test") == "portChecker": elif self.getCmdlineValue("test") == "portChecker":
self.test_parser.add_argument( self.test_parser.add_argument(
'--func_name', help='Name of open port checker function', '--func-name', help='Name of open port checker function',
default=None, metavar='func_name' default=None, metavar='func_name'
) )
return back return back

View file

@ -837,7 +837,7 @@ class SitePlugin(object):
class ConfigPlugin(object): class ConfigPlugin(object):
def createArguments(self): def createArguments(self):
group = self.parser.add_argument_group("Bigfile plugin") group = self.parser.add_argument_group("Bigfile plugin")
group.add_argument('--autodownload_bigfile_size_limit', help='Also download bigfiles smaller than this limit if help distribute option is checked', default=10, metavar="MB", type=int) group.add_argument('--autodownload-bigfile-size-limit', help='Also download bigfiles smaller than this limit if help distribute option is checked', default=10, metavar="MB", type=int)
group.add_argument('--bigfile_size_limit', help='Maximum size of downloaded big files', default=False, metavar="MB", type=int) group.add_argument('--bigfile-size-limit', help='Maximum size of downloaded big files', default=False, metavar="MB", type=int)
return super(ConfigPlugin, self).createArguments() return super(ConfigPlugin, self).createArguments()

View file

@ -6,7 +6,7 @@ import time
class ChartDb(Db): class ChartDb(Db):
def __init__(self): def __init__(self):
self.version = 2 self.version = 2
super(ChartDb, self).__init__(self.getSchema(), "%s/chart.db" % config.data_dir) super(ChartDb, self).__init__(self.getSchema(), config.start_dir / 'chart.db')
self.foreign_keys = True self.foreign_keys = True
self.checkTables() self.checkTables()
self.sites = self.loadSites() self.sites = self.loadSites()

View file

@ -38,7 +38,7 @@ class SiteManagerPlugin(object):
block_details = None block_details = None
if block_details: if block_details:
raise Exception("Site blocked: %s" % html.escape(block_details.get("reason", "unknown reason"))) raise Exception(f'Site blocked: {html.escape(block_details.get("reason", "unknown reason"))}')
else: else:
return super(SiteManagerPlugin, self).add(address, *args, **kwargs) return super(SiteManagerPlugin, self).add(address, *args, **kwargs)
@ -56,14 +56,11 @@ class UiWebsocketPlugin(object):
@flag.no_multiuser @flag.no_multiuser
def actionMuteAdd(self, to, auth_address, cert_user_id, reason): def actionMuteAdd(self, to, auth_address, cert_user_id, reason):
if "ADMIN" in self.getPermissions(to): self.cmd(
self.cbMuteAdd(to, auth_address, cert_user_id, reason) "prompt",
else: [_["Remove all content from <b>%s</b>?"] % html.escape(cert_user_id), reason, _["Mute"]],
self.cmd( lambda res: self.cbMuteAdd(to, auth_address, cert_user_id, res if res else reason)
"confirm", )
[_["Hide all content from <b>%s</b>?"] % html.escape(cert_user_id), _["Mute"]],
lambda res: self.cbMuteAdd(to, auth_address, cert_user_id, reason)
)
@flag.no_multiuser @flag.no_multiuser
def cbMuteRemove(self, to, auth_address): def cbMuteRemove(self, to, auth_address):
@ -207,18 +204,40 @@ class SiteStoragePlugin(object):
# Check if any of the adresses are in the mute list # Check if any of the adresses are in the mute list
for auth_address in matches: for auth_address in matches:
if filter_storage.isMuted(auth_address): if filter_storage.isMuted(auth_address):
self.log.debug("Mute match: %s, ignoring %s" % (auth_address, inner_path)) self.log.debug(f'Mute match: {auth_address}, ignoring {inner_path}')
return False return False
return super(SiteStoragePlugin, self).updateDbFile(inner_path, file=file, cur=cur) return super(SiteStoragePlugin, self).updateDbFile(inner_path, file=file, cur=cur)
def onUpdated(self, inner_path, file=None): def onUpdated(self, inner_path, file=None):
file_path = "%s/%s" % (self.site.address, inner_path) file_path = f'{self.site.address}/{inner_path}'
if file_path in filter_storage.file_content["includes"]: if file_path in filter_storage.file_content['includes']:
self.log.debug("Filter file updated: %s" % inner_path) self.log.debug('Filter file updated: {inner_path}')
filter_storage.includeUpdateAll() filter_storage.includeUpdateAll()
return super(SiteStoragePlugin, self).onUpdated(inner_path, file=file) return super(SiteStoragePlugin, self).onUpdated(inner_path, file=file)
@PluginManager.registerTo("Site")
class SitePlugin(object):
def needFile(self, inner_path, update=False, blocking=True, peer=None, priority=0):
self.log.debug(f'needFile {inner_path}')
matches = re.findall('/(1[A-Za-z0-9]{26,35})/', inner_path)
for auth_address in matches:
if filter_storage.isMuted(auth_address):
self.log.info(f'Mute match in Site.needFile: {auth_address}, ignoring {inner_path}')
return False
return super(SitePlugin, self).needFile(inner_path, update, blocking, peer, priority)
@PluginManager.registerTo("FileRequest")
class FileRequestPlugin:
def actionUpdate(self, params):
inner_path = params.get('inner_path', '')
matches = re.findall('/(1[A-Za-z0-9]{26,35})/', inner_path)
for auth_address in matches:
if filter_storage.isMuted(auth_address):
self.log.info(f'Mute match in FileRequest.actionUpdate: {auth_address}, ignoring {inner_path}')
self.response({'ok': f'Thanks, file {inner_path} updated!'})
return False
return super(FileRequestPlugin, self).actionUpdate(params)
@PluginManager.registerTo("UiRequest") @PluginManager.registerTo("UiRequest")
class UiRequestPlugin(object): class UiRequestPlugin(object):

View file

@ -14,7 +14,7 @@ from util import helper
class ContentFilterStorage(object): class ContentFilterStorage(object):
def __init__(self, site_manager): def __init__(self, site_manager):
self.log = logging.getLogger("ContentFilterStorage") self.log = logging.getLogger("ContentFilterStorage")
self.file_path = "%s/filters.json" % config.data_dir self.file_path = config.config_dir / 'filters.json'
self.site_manager = site_manager self.site_manager = site_manager
self.file_content = self.load() self.file_content = self.load()
@ -36,12 +36,12 @@ class ContentFilterStorage(object):
def load(self): def load(self):
# Rename previously used mutes.json -> filters.json # Rename previously used mutes.json -> filters.json
if os.path.isfile("%s/mutes.json" % config.data_dir): if (config.config_dir / 'mutes.json').is_file():
self.log.info("Renaming mutes.json to filters.json...") self.log.info("Renaming mutes.json to filters.json...")
os.rename("%s/mutes.json" % config.data_dir, self.file_path) os.rename(config.config_dir / 'mutes.json', self.file_path)
if os.path.isfile(self.file_path): if self.file_path.is_file():
try: try:
return json.load(open(self.file_path)) return json.load(self.file_path.open())
except Exception as err: except Exception as err:
self.log.error("Error loading filters.json: %s" % err) self.log.error("Error loading filters.json: %s" % err)
return None return None
@ -158,7 +158,7 @@ class ContentFilterStorage(object):
dir_inner_path = helper.getDirname(row["inner_path"]) dir_inner_path = helper.getDirname(row["inner_path"])
for file_name in site.storage.walk(dir_inner_path): for file_name in site.storage.walk(dir_inner_path):
if action == "remove": if action == "remove":
site.storage.onUpdated(dir_inner_path + file_name, False) site.storage.delete(dir_inner_path + file_name)
else: else:
site.storage.onUpdated(dir_inner_path + file_name) site.storage.onUpdated(dir_inner_path + file_name)
site.onFileDone(dir_inner_path + file_name) site.onFileDone(dir_inner_path + file_name)

View file

@ -44,8 +44,8 @@ class UiRequestPlugin(object):
if ".zip/" in path or ".tar.gz/" in path: if ".zip/" in path or ".tar.gz/" in path:
file_obj = None file_obj = None
path_parts = self.parsePath(path) path_parts = self.parsePath(path)
file_path = "%s/%s/%s" % (config.data_dir, path_parts["address"], path_parts["inner_path"]) file_path = config.data_dir / path_parts["address"] / path_parts["inner_path"]
match = re.match("^(.*\.(?:tar.gz|zip))/(.*)", file_path) match = re.match(r"^(.*\.(?:tar.gz|zip))/(.*)", file_path)
archive_path, path_within = match.groups() archive_path, path_within = match.groups()
if archive_path not in archive_cache: if archive_path not in archive_cache:
site = self.server.site_manager.get(path_parts["address"]) site = self.server.site_manager.get(path_parts["address"])
@ -99,7 +99,7 @@ class UiRequestPlugin(object):
class SiteStoragePlugin(object): class SiteStoragePlugin(object):
def isFile(self, inner_path): def isFile(self, inner_path):
if ".zip/" in inner_path or ".tar.gz/" in inner_path: if ".zip/" in inner_path or ".tar.gz/" in inner_path:
match = re.match("^(.*\.(?:tar.gz|zip))/(.*)", inner_path) match = re.match(r"^(.*\.(?:tar.gz|zip))/(.*)", inner_path)
archive_inner_path, path_within = match.groups() archive_inner_path, path_within = match.groups()
return super(SiteStoragePlugin, self).isFile(archive_inner_path) return super(SiteStoragePlugin, self).isFile(archive_inner_path)
else: else:
@ -127,7 +127,7 @@ class SiteStoragePlugin(object):
def walk(self, inner_path, *args, **kwags): def walk(self, inner_path, *args, **kwags):
if ".zip" in inner_path or ".tar.gz" in inner_path: if ".zip" in inner_path or ".tar.gz" in inner_path:
match = re.match("^(.*\.(?:tar.gz|zip))(.*)", inner_path) match = re.match(r"^(.*\.(?:tar.gz|zip))(.*)", inner_path)
archive_inner_path, path_within = match.groups() archive_inner_path, path_within = match.groups()
archive = self.openArchive(archive_inner_path) archive = self.openArchive(archive_inner_path)
path_within = path_within.lstrip("/") path_within = path_within.lstrip("/")
@ -151,7 +151,7 @@ class SiteStoragePlugin(object):
def list(self, inner_path, *args, **kwags): def list(self, inner_path, *args, **kwags):
if ".zip" in inner_path or ".tar.gz" in inner_path: if ".zip" in inner_path or ".tar.gz" in inner_path:
match = re.match("^(.*\.(?:tar.gz|zip))(.*)", inner_path) match = re.match(r"^(.*\.(?:tar.gz|zip))(.*)", inner_path)
archive_inner_path, path_within = match.groups() archive_inner_path, path_within = match.groups()
archive = self.openArchive(archive_inner_path) archive = self.openArchive(archive_inner_path)
path_within = path_within.lstrip("/") path_within = path_within.lstrip("/")
@ -178,7 +178,7 @@ class SiteStoragePlugin(object):
def read(self, inner_path, mode="rb", **kwargs): def read(self, inner_path, mode="rb", **kwargs):
if ".zip/" in inner_path or ".tar.gz/" in inner_path: if ".zip/" in inner_path or ".tar.gz/" in inner_path:
match = re.match("^(.*\.(?:tar.gz|zip))(.*)", inner_path) match = re.match(r"^(.*\.(?:tar.gz|zip))(.*)", inner_path)
archive_inner_path, path_within = match.groups() archive_inner_path, path_within = match.groups()
archive = self.openArchive(archive_inner_path) archive = self.openArchive(archive_inner_path)
path_within = path_within.lstrip("/") path_within = path_within.lstrip("/")

View file

@ -247,7 +247,7 @@ class SitePlugin(object):
class ConfigPlugin(object): class ConfigPlugin(object):
def createArguments(self): def createArguments(self):
group = self.parser.add_argument_group("OptionalManager plugin") group = self.parser.add_argument_group("OptionalManager plugin")
group.add_argument('--optional_limit', help='Limit total size of optional files', default="10%", metavar="GB or free space %") group.add_argument('--optional-limit', help='Limit total size of optional files', default="10%", metavar="GB or free space %")
group.add_argument('--optional_limit_exclude_minsize', help='Exclude files larger than this limit from optional size limit calculation', default=20, metavar="MB", type=int) group.add_argument('--optional-limit-exclude-minsize', help='Exclude files larger than this limit from optional size limit calculation', default=20, metavar="MB", type=int)
return super(ConfigPlugin, self).createArguments() return super(ConfigPlugin, self).createArguments()

View file

@ -14,8 +14,7 @@ class WsLogStreamer(logging.StreamHandler):
self.ui_websocket = ui_websocket self.ui_websocket = ui_websocket
if filter: if filter:
if not SafeRe.isSafePattern(filter): SafeRe.guard(filter)
raise Exception("Not a safe prex pattern")
self.filter_re = re.compile(".*" + filter) self.filter_re = re.compile(".*" + filter)
else: else:
self.filter_re = None self.filter_re = None
@ -55,7 +54,7 @@ class UiWebsocketPlugin(object):
pos_start = log_file.tell() pos_start = log_file.tell()
lines = [] lines = []
if filter: if filter:
assert SafeRe.isSafePattern(filter) SafeRe.guard(filter)
filter_re = re.compile(".*" + filter) filter_re = re.compile(".*" + filter)
last_match = False last_match = False

View file

@ -12,6 +12,7 @@ import urllib.parse
import gevent import gevent
import util import util
import main
from Config import config from Config import config
from Plugin import PluginManager from Plugin import PluginManager
from Debug import Debug from Debug import Debug
@ -115,11 +116,11 @@ class UiWebsocketPlugin(object):
local_html = "" local_html = ""
peer_ips = [peer.key for peer in site.getConnectablePeers(20, allow_private=False)] peer_ips = [peer.key for peer in site.getConnectablePeers(20, allow_private=False)]
self_onion = main.file_server.tor_manager.site_onions.get(site.address, None)
if self_onion is not None:
peer_ips.append(self_onion+'.onion')
peer_ips.sort(key=lambda peer_ip: ".onion:" in peer_ip) peer_ips.sort(key=lambda peer_ip: ".onion:" in peer_ip)
copy_link = "http://127.0.0.1:43110/%s/?zeronet_peers=%s" % ( copy_link = f'http://127.0.0.1:43110/{site.address}/?zeronet_peers={",".join(peer_ips)}'
site.content_manager.contents.get("content.json", {}).get("domain", site.address),
",".join(peer_ips)
)
body.append(_(""" body.append(_("""
<li> <li>
@ -175,6 +176,7 @@ class UiWebsocketPlugin(object):
{_[Files]} {_[Files]}
<a href='/list/{site.address}' class='link-right link-outline' id="browse-files">{_[Browse files]}</a> <a href='/list/{site.address}' class='link-right link-outline' id="browse-files">{_[Browse files]}</a>
<small class="label-right"> <small class="label-right">
<a href='#Site+directory' id='link-directory' class='link-right'>{_[Open site directory]}</a>
<a href='/ZeroNet-Internal/Zip?address={site.address}' id='link-zip' class='link-right' download='site.zip'>{_[Save as .zip]}</a> <a href='/ZeroNet-Internal/Zip?address={site.address}' id='link-zip' class='link-right' download='site.zip'>{_[Save as .zip]}</a>
</small> </small>
</label> </label>
@ -414,43 +416,87 @@ class UiWebsocketPlugin(object):
class_pause = "hidden" class_pause = "hidden"
class_resume = "" class_resume = ""
dashboard = config.homepage
dsite = self.user.sites.get(dashboard, None)
if not dsite:
print('No dashboard found, cannot favourite')
class_favourite = "hidden"
class_unfavourite = "hidden"
elif not dsite.get('settings', {}).get('favorite_sites', {}).get(self.site.address, False):
class_favourite = ""
class_unfavourite = "hidden"
else:
class_favourite = "hidden"
class_unfavourite = ""
body.append(_(""" body.append(_("""
<li> <li>
<label>{_[Site control]}</label> <label>{_[Site control]}</label>
<a href='#Update' class='button noupdate' id='button-update'>{_[Update]}</a> <a href='#Update' class='button noupdate' id='button-update'>{_[Update]}</a>
<a href='#Pause' class='button {class_pause}' id='button-pause'>{_[Pause]}</a> <a href='#Pause' class='button {class_pause}' id='button-pause'>{_[Pause]}</a>
<a href='#Favourite' class='button {class_favourite}' id='button-favourite'>{_[Favourite]}</a>
<a href='#Unfavourite' class='button {class_unfavourite}' id='button-unfavourite'>{_[Unfavourite]}</a>
<a href='#Resume' class='button {class_resume}' id='button-resume'>{_[Resume]}</a> <a href='#Resume' class='button {class_resume}' id='button-resume'>{_[Resume]}</a>
<a href='#Delete' class='button noupdate' id='button-delete'>{_[Delete]}</a> <a href='#Delete' class='button noupdate' id='button-delete'>{_[Delete]}</a>
</li> </li>
""")) """))
donate_key = site.content_manager.contents.get("content.json", {}).get("donate", True)
site_address = self.site.address site_address = self.site.address
body.append(_(""" body.append(_("""
<li> <li>
<label>{_[Site address]}</label><br> <label>{_[Site address]}</label><br>
<div class='flex'> <div class='flex'>
<span class='input text disabled'>{site_address}</span> <span class='input text disabled'>{site_address}</span>
"""))
if donate_key == False or donate_key == "":
pass
elif (type(donate_key) == str or type(donate_key) == str) and len(donate_key) > 0:
body.append(_("""
</div> </div>
</li> </li>
"""))
donate_generic = site.content_manager.contents.get("content.json", {}).get("donate", None) or site.content_manager.contents.get("content.json", {}).get("donate-generic", None)
donate_btc = site.content_manager.contents.get("content.json", {}).get("donate-btc", None)
donate_xmr = site.content_manager.contents.get("content.json", {}).get("donate-xmr", None)
donate_ada = site.content_manager.contents.get("content.json", {}).get("donate-ada", None)
donate_enabled = bool(donate_generic or donate_btc or donate_xmr or donate_ada)
if donate_enabled:
body.append(_("""
<li> <li>
<label>{_[Donate]}</label><br> <label>{_[Donate]}</label><br>
<div class='flex'>
{donate_key}
""")) """))
else: if donate_generic:
body.append(_(""" body.append(_("""
<a href='bitcoin:{site_address}' class='button' id='button-donate'>{_[Donate]}</a> <div class='flex'>
{donate_generic}
</div>
""")) """))
body.append(_(""" if donate_btc:
</div> body.append(_("""
<div class='flex'>
<span style="font-size:90%">{donate_btc}</span><br/>
</div>
<div class='flex'>
<a href='bitcoin:{donate_btc}' class='button'>{_[Donate BTC]}</a>
</div>
"""))
if donate_xmr:
body.append(_("""
<div class='flex'>
<span style="font-size:90%">{donate_xmr}</span><br/>
</div>
<div class='flex'>
<a href='monero:{donate_xmr}' class='button'>{_[Donate Monero]}</a>
</div>
"""))
if donate_ada:
body.append(_("""
<div class='flex'>
<span style="font-size:90%">{donate_ada}</span><br/>
</div>
<div class='flex'>
<a href='web+cardano:{donate_ada}' class='button'>{_[Donate Ada/Cardano]}</a>
</div>
"""))
if donate_enabled:
body.append(_("""
</li> </li>
""")) """))
def sidebarRenderOwnedCheckbox(self, body, site): def sidebarRenderOwnedCheckbox(self, body, site):
if self.site.settings["own"]: if self.site.settings["own"]:
@ -556,9 +602,9 @@ class UiWebsocketPlugin(object):
def downloadGeoLiteDb(self, db_path): def downloadGeoLiteDb(self, db_path):
import gzip import gzip
import shutil import shutil
from util import helper import requests
if config.offline: if config.offline or config.tor == 'always':
return False return False
self.log.info("Downloading GeoLite2 City database...") self.log.info("Downloading GeoLite2 City database...")
@ -571,19 +617,18 @@ class UiWebsocketPlugin(object):
downloadl_err = None downloadl_err = None
try: try:
# Download # Download
response = helper.httpRequest(db_url) response = requests.get(db_url, stream=True)
data_size = response.getheader('content-length') data_size = response.headers.get('content-length')
if data_size is None:
data.write(response.content)
data_size = int(data_size)
data_recv = 0 data_recv = 0
data = io.BytesIO() data = io.BytesIO()
while True: for buff in response.iter_content(chunk_size=1024 * 512):
buff = response.read(1024 * 512)
if not buff:
break
data.write(buff) data.write(buff)
data_recv += 1024 * 512 data_recv += 1024 * 512
if data_size: progress = int(float(data_recv) / data_size * 100)
progress = int(float(data_recv) / int(data_size) * 100) self.cmd("progress", ["geolite-info", _["Downloading GeoLite2 City database (one time only, ~20MB)..."], progress])
self.cmd("progress", ["geolite-info", _["Downloading GeoLite2 City database (one time only, ~20MB)..."], progress])
self.log.info("GeoLite2 City database downloaded (%s bytes), unpacking..." % data.tell()) self.log.info("GeoLite2 City database downloaded (%s bytes), unpacking..." % data.tell())
data.seek(0) data.seek(0)
@ -641,7 +686,7 @@ class UiWebsocketPlugin(object):
if sys.platform == "linux": if sys.platform == "linux":
sys_db_paths += ['/usr/share/GeoIP/' + db_name] sys_db_paths += ['/usr/share/GeoIP/' + db_name]
data_dir_db_path = os.path.join(config.data_dir, db_name) data_dir_db_path = config.start_dir / db_name
db_paths = sys_db_paths + [data_dir_db_path] db_paths = sys_db_paths + [data_dir_db_path]
@ -738,9 +783,6 @@ class UiWebsocketPlugin(object):
@flag.admin @flag.admin
@flag.no_multiuser @flag.no_multiuser
def actionSiteSetOwned(self, to, owned): def actionSiteSetOwned(self, to, owned):
if self.site.address == config.updatesite:
return {"error": "You can't change the ownership of the updater site"}
self.site.settings["own"] = bool(owned) self.site.settings["own"] = bool(owned)
self.site.updateWebsocket(owned=owned) self.site.updateWebsocket(owned=owned)
return "ok" return "ok"

View file

@ -1,9 +1,13 @@
{ {
"Total peers": "Łącznie użytkowników równorzędnych",
"Connected peers": "Łącznie połączonych użytkowników równorzędnych",
"Peers": "Użytkownicy równorzędni", "Peers": "Użytkownicy równorzędni",
"Connected": "Połączony", "Connected": "Połączony",
"Connectable": "Możliwy do podłączenia", "Connectable": "Do połączenia",
"Connectable peers": "Połączeni użytkownicy równorzędni", "Connectable peers": "Połączeni użytkownicy równorzędni",
"Copy to clipboard": "Kopiuj do schowka",
"Data transfer": "Transfer danych", "Data transfer": "Transfer danych",
"Received": "Odebrane", "Received": "Odebrane",
"Received bytes": "Odebrany bajty", "Received bytes": "Odebrany bajty",
@ -11,7 +15,7 @@
"Sent bytes": "Wysłane bajty", "Sent bytes": "Wysłane bajty",
"Files": "Pliki", "Files": "Pliki",
"Total": "Sumarycznie", "Total": "Łącznie",
"Image": "Obraz", "Image": "Obraz",
"Other": "Inne", "Other": "Inne",
"User data": "Dane użytkownika", "User data": "Dane użytkownika",
@ -36,6 +40,10 @@
"Identity address": "Adres identyfikacyjny", "Identity address": "Adres identyfikacyjny",
"Change": "Zmień", "Change": "Zmień",
"Needs to be updated": "Muszą zostać zaktualizowane",
"Download previous files": "Pobierz wszystkie pliki",
"Help distribute added optional files": "Pomóż rozpowszechniać wszystkie pliki",
"Auto download big file size limit": "Limit dla automatycznego pobierania dużych plików",
"Site control": "Kontrola strony", "Site control": "Kontrola strony",
"Update": "Zaktualizuj", "Update": "Zaktualizuj",
@ -56,11 +64,15 @@
"Site title": "Tytuł strony", "Site title": "Tytuł strony",
"Site description": "Opis strony", "Site description": "Opis strony",
"Save site settings": "Zapisz ustawienia strony", "Save site settings": "Zapisz ustawienia strony",
"Save as .zip": "Zapisz jako .zip",
"Browse files": "Przeglądaj pliki",
"Add saved private key": "Dodaj klucz prywatny",
"Content publishing": "Publikowanie treści", "Content publishing": "Publikowanie treści",
"Choose": "Wybierz", "Choose": "Wybierz",
"Sign": "Podpisz", "Sign": "Podpisz",
"Publish": "Opublikuj", "Publish": "Opublikuj",
"Sign and publish": "Zapisz i opublikuj",
"This function is disabled on this proxy": "Ta funkcja jest zablokowana w tym proxy", "This function is disabled on this proxy": "Ta funkcja jest zablokowana w tym proxy",
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "Błąd ściągania bazy danych GeoLite2 City: {}!<br>Proszę ściągnąć ją recznie i wypakować do katalogu danych:<br>{}", "GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "Błąd ściągania bazy danych GeoLite2 City: {}!<br>Proszę ściągnąć ją recznie i wypakować do katalogu danych:<br>{}",

View file

@ -1,23 +0,0 @@
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

@ -1,201 +0,0 @@
class Console extends Class
constructor: (@sidebar) ->
@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) =>
if message.cmd == "logLineAdd" and message.params.stream_id == @stream_id
@addLines(message.params.lines)
else
handleMessageWebsocket_original(message)
$(window).on "hashchange", =>
if window.top.location.hash.startsWith("#ZeroNet:Console")
@open()
if window.top.location.hash.startsWith("#ZeroNet:Console")
setTimeout (=> @open()), 10
createHtmltag: ->
if not @container
@container = $("""
<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">
<div class="mynode"></div>
<div class="peers">
<div class="peer"><div class="line"></div><a href="#" class="icon">\u25BD</div></div>
</div>
</div>
</div>
</div>
""")
@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
@text.stop()
RateLimit 300, @checkTextIsBottom
@text.is_bottom = true
@container.appendTo(document.body)
@tag = @container.find(".console")
for tab_type in @tab_types
tab = $("<a></a>", {href: "#", "data-filter": tab_type.filter, "data-title": tab_type.title}).text(tab_type.title)
if tab_type.filter == @tab_active
tab.addClass("active")
tab.on("click", @handleTabClick)
if window.top.location.hash.endsWith(tab_type.title)
@log "Triggering click on", tab
tab.trigger("click")
@tabs.append(tab)
@container.on "mousedown touchend touchcancel", (e) =>
if e.target != e.currentTarget
return true
@log "closing"
if $(document.body).hasClass("body-console")
@close()
return true
@loadConsoleText()
checkTextIsBottom: =>
@text.is_bottom = Math.round(@text_elem.scrollTop + @text_elem.clientHeight) >= @text_elem.scrollHeight - 15
toColor: (text, saturation=60, lightness=70) ->
hash = 0
for i in [0..text.length-1]
hash += text.charCodeAt(i)*i
hash = hash % 1777
return "hsl(" + (hash % 360) + ",#{saturation}%,#{lightness}%)";
formatLine: (line) =>
match = line.match(/(\[.*?\])[ ]+(.*?)[ ]+(.*?)[ ]+(.*)/)
if not match
return line.replace(/\</g, "&lt;").replace(/\>/g, "&gt;")
[line, added, level, module, text] = line.match(/(\[.*?\])[ ]+(.*?)[ ]+(.*?)[ ]+(.*)/)
added = "<span style='color: #dfd0fa'>#{added}</span>"
level = "<span style='color: #{@toColor(level, 100)};'>#{level}</span>"
module = "<span style='color: #{@toColor(module, 60)}; font-weight: bold;'>#{module}</span>"
text = text.replace(/(Site:[A-Za-z0-9\.]+)/g, "<span style='color: #AAAAFF'>$1</span>")
text = text.replace(/\</g, "&lt;").replace(/\>/g, "&gt;")
#text = text.replace(/( [0-9\.]+(|s|ms))/g, "<span style='color: #FFF;'>$1</span>")
return "#{added} #{level} #{module} #{text}"
addLines: (lines, animate=true) =>
html_lines = []
@logStart "formatting"
for line in lines
html_lines.push @formatLine(line)
@logEnd "formatting"
@logStart "adding"
@text.append(html_lines.join("<br>") + "<br>")
@logEnd "adding"
if @text.is_bottom and animate
@text.stop().animate({scrollTop: @text_elem.scrollHeight - @text_elem.clientHeight + 1}, 600, 'easeInOutCubic')
loadConsoleText: =>
@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: =>
@sidebar.startDrag()
@sidebar.moved("y")
@sidebar.fixbutton_targety = @sidebar.page_height - @sidebar.fixbutton_inity - 50
@sidebar.stopDrag()
onOpened: =>
@sidebar.onClosed()
@log "onOpened"
onClosed: =>
$(document.body).removeClass("body-console")
if @stream_id
@sidebar.wrapper.ws.cmd "consoleLogStreamRemove", {stream_id: @stream_id}
cleanup: =>
if @container
@container.remove()
@container = null
stopDragY: =>
# Animate sidebar and iframe
if @sidebar.fixbutton_targety == @sidebar.fixbutton_inity
# Closed
targety = 0
@opened = false
else
# Opened
targety = @sidebar.fixbutton_targety - @sidebar.fixbutton_inity
@onOpened()
@opened = true
# Revent sidebar transitions
if @tag
@tag.css("transition", "0.5s ease-out")
@tag.css("transform", "translateY(#{targety}px)").one transitionEnd, =>
@tag.css("transition", "")
if not @opened
@cleanup()
# Revert body transformations
@log "stopDragY", "opened:", @opened, targety
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)
window.top.location.hash = "#ZeroNet:Console:" + elem.data("title")
return false
window.Console = Console

View file

@ -1,49 +0,0 @@
class Menu
constructor: (@button) ->
@elem = $(".menu.template").clone().removeClass("template")
@elem.appendTo("body")
@items = []
show: ->
if window.visible_menu and window.visible_menu.button[0] == @button[0] # Same menu visible then hide it
window.visible_menu.hide()
@hide()
else
button_pos = @button.offset()
left = button_pos.left
@elem.css({"top": button_pos.top+@button.outerHeight(), "left": left})
@button.addClass("menu-active")
@elem.addClass("visible")
if @elem.position().left + @elem.width() + 20 > window.innerWidth
@elem.css("left", window.innerWidth - @elem.width() - 20)
if window.visible_menu then window.visible_menu.hide()
window.visible_menu = @
hide: ->
@elem.removeClass("visible")
@button.removeClass("menu-active")
window.visible_menu = null
addItem: (title, cb) ->
item = $(".menu-item.template", @elem).clone().removeClass("template")
item.html(title)
item.on "click", =>
if not cb(item)
@hide()
return false
item.appendTo(@elem)
@items.push item
return item
log: (args...) ->
console.log "[Menu]", args...
window.Menu = Menu
# Hide menu on outside click
$("body").on "click", (e) ->
if window.visible_menu and e.target != window.visible_menu.button[0] and $(e.target).parent()[0] != window.visible_menu.elem[0]
window.visible_menu.hide()

View file

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

View file

@ -1,14 +0,0 @@
limits = {}
call_after_interval = {}
window.RateLimit = (interval, fn) ->
if not limits[fn]
call_after_interval[fn] = false
fn() # First call is not delayed
limits[fn] = setTimeout (->
if call_after_interval[fn]
fn()
delete limits[fn]
delete call_after_interval[fn]
), interval
else # Called within iterval, delay the call
call_after_interval[fn] = true

View file

@ -1,644 +0,0 @@
class Sidebar extends Class
constructor: (@wrapper) ->
@tag = null
@container = null
@opened = false
@width = 410
@console = new Console(@)
@fixbutton = $(".fixbutton")
@fixbutton_addx = 0
@fixbutton_addy = 0
@fixbutton_initx = 0
@fixbutton_inity = 15
@fixbutton_targetx = 0
@move_lock = null
@page_width = $(window).width()
@page_height = $(window).height()
@frame = $("#inner-iframe")
@initFixbutton()
@dragStarted = 0
@globe = null
@preload_html = null
@original_set_site_info = @wrapper.setSiteInfo # We going to override this, save the original
# Start in opened state for debugging
if window.top.location.hash == "#ZeroNet:OpenSidebar"
@startDrag()
@moved("x")
@fixbutton_targetx = @fixbutton_initx - @width
@stopDrag()
initFixbutton: ->
# Detect dragging
@fixbutton.on "mousedown touchstart", (e) =>
if e.button > 0 # Right or middle click
return
e.preventDefault()
# Disable previous listeners
@fixbutton.off "click touchend touchcancel"
# Make sure its not a click
@dragStarted = (+ new Date)
# Fullscreen drag bg to capture mouse events over iframe
$(".drag-bg").remove()
$("<div class='drag-bg'></div>").appendTo(document.body)
$("body").one "mousemove touchmove", (e) =>
mousex = e.pageX
mousey = e.pageY
if not mousex
mousex = e.originalEvent.touches[0].pageX
mousey = e.originalEvent.touches[0].pageY
@fixbutton_addx = @fixbutton.offset().left - mousex
@fixbutton_addy = @fixbutton.offset().top - mousey
@startDrag()
@fixbutton.parent().on "click touchend touchcancel", (e) =>
if (+ new Date) - @dragStarted < 100
window.top.location = @fixbutton.find(".fixbutton-bg").attr("href")
@stopDrag()
@resized()
$(window).on "resize", @resized
resized: =>
@page_width = $(window).width()
@page_height = $(window).height()
@fixbutton_initx = @page_width - 75 # Initial x position
if @opened
@fixbutton.css
left: @fixbutton_initx - @width
else
@fixbutton.css
left: @fixbutton_initx
# Start dragging the fixbutton
startDrag: ->
#@move_lock = "x" # Temporary until internals not finished
@log "startDrag", @fixbutton_initx, @fixbutton_inity
@fixbutton_targetx = @fixbutton_initx # Fallback x position
@fixbutton_targety = @fixbutton_inity # Fallback y position
@fixbutton.addClass("dragging")
# IE position wrap fix
if navigator.userAgent.indexOf('MSIE') != -1 or navigator.appVersion.indexOf('Trident/') > 0
@fixbutton.css("pointer-events", "none")
# Don't go to homepage
@fixbutton.one "click", (e) =>
@stopDrag()
@fixbutton.removeClass("dragging")
moved_x = Math.abs(@fixbutton.offset().left - @fixbutton_initx)
moved_y = Math.abs(@fixbutton.offset().top - @fixbutton_inity)
if moved_x > 5 or moved_y > 10
# If moved more than some pixel the button then don't go to homepage
e.preventDefault()
# Animate drag
@fixbutton.parents().on "mousemove touchmove", @animDrag
@fixbutton.parents().on "mousemove touchmove" ,@waitMove
# Stop dragging listener
@fixbutton.parents().one "mouseup touchend touchcancel", (e) =>
e.preventDefault()
@stopDrag()
# Wait for moving the fixbutton
waitMove: (e) =>
document.body.style.perspective = "1000px"
document.body.style.height = "100%"
document.body.style.willChange = "perspective"
document.documentElement.style.height = "100%"
#$(document.body).css("backface-visibility", "hidden").css("perspective", "1000px").css("height", "900px")
# $("iframe").css("backface-visibility", "hidden")
moved_x = Math.abs(parseInt(@fixbutton[0].style.left) - @fixbutton_targetx)
moved_y = Math.abs(parseInt(@fixbutton[0].style.top) - @fixbutton_targety)
if moved_x > 5 and (+ new Date) - @dragStarted + moved_x > 50
@moved("x")
@fixbutton.stop().animate {"top": @fixbutton_inity}, 1000
@fixbutton.parents().off "mousemove touchmove" ,@waitMove
else if moved_y > 5 and (+ new Date) - @dragStarted + moved_y > 50
@moved("y")
@fixbutton.parents().off "mousemove touchmove" ,@waitMove
moved: (direction) ->
@log "Moved", direction
@move_lock = direction
if direction == "y"
$(document.body).addClass("body-console")
return @console.createHtmltag()
@createHtmltag()
$(document.body).addClass("body-sidebar")
@container.on "mousedown touchend touchcancel", (e) =>
if e.target != e.currentTarget
return true
@log "closing"
if $(document.body).hasClass("body-sidebar")
@close()
return true
$(window).off "resize"
$(window).on "resize", =>
$(document.body).css "height", $(window).height()
@scrollable()
@resized()
# Override setsiteinfo to catch changes
@wrapper.setSiteInfo = (site_info) =>
@setSiteInfo(site_info)
@original_set_site_info.apply(@wrapper, arguments)
# Preload world.jpg
img = new Image();
img.src = "/uimedia/globe/world.jpg";
setSiteInfo: (site_info) ->
RateLimit 1500, =>
@updateHtmlTag()
RateLimit 30000, =>
@displayGlobe()
# Create the sidebar html tag
createHtmltag: ->
@when_loaded = $.Deferred()
if not @container
@container = $("""
<div class="sidebar-container"><div class="sidebar scrollable"><div class="content-wrapper"><div class="content">
</div></div></div></div>
""")
@container.appendTo(document.body)
@tag = @container.find(".sidebar")
@updateHtmlTag()
@scrollable = window.initScrollable()
updateHtmlTag: ->
if @preload_html
@setHtmlTag(@preload_html)
@preload_html = null
else
@wrapper.ws.cmd "sidebarGetHtmlTag", {}, @setHtmlTag
setHtmlTag: (res) =>
if @tag.find(".content").children().length == 0 # First update
@log "Creating content"
@container.addClass("loaded")
morphdom(@tag.find(".content")[0], '<div class="content">'+res+'</div>')
# @scrollable()
@when_loaded.resolve()
else # Not first update, patch the html to keep unchanged dom elements
morphdom @tag.find(".content")[0], '<div class="content">'+res+'</div>', {
onBeforeMorphEl: (from_el, to_el) -> # Ignore globe loaded state
if from_el.className == "globe" or from_el.className.indexOf("noupdate") >= 0
return false
else
return true
}
# 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-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) =>
@wrapper.notifications.add "privatekey", "done", "Saved private key removed", 5000
return false
# Use requested address for browse files urls
@tag.find("#browse-files").attr("href", document.location.pathname.replace(/(\/.*?(\/|$)).*$/, "/list$1"))
animDrag: (e) =>
mousex = e.pageX
mousey = e.pageY
if not mousex and e.originalEvent.touches
mousex = e.originalEvent.touches[0].pageX
mousey = e.originalEvent.touches[0].pageY
overdrag = @fixbutton_initx - @width - mousex
if overdrag > 0 # Overdragged
overdrag_percent = 1 + overdrag/300
mousex = (mousex + (@fixbutton_initx-@width)*overdrag_percent)/(1+overdrag_percent)
targetx = @fixbutton_initx - mousex - @fixbutton_addx
targety = @fixbutton_inity - mousey - @fixbutton_addy
if @move_lock == "x"
targety = @fixbutton_inity
else if @move_lock == "y"
targetx = @fixbutton_initx
if not @move_lock or @move_lock == "x"
@fixbutton[0].style.left = (mousex + @fixbutton_addx) + "px"
if @tag
@tag[0].style.transform = "translateX(#{0 - targetx}px)"
if not @move_lock or @move_lock == "y"
@fixbutton[0].style.top = (mousey + @fixbutton_addy) + "px"
if @console.tag
@console.tag[0].style.transform = "translateY(#{0 - targety}px)"
#if @move_lock == "x"
# @fixbutton[0].style.left = "#{@fixbutton_targetx} px"
#@fixbutton[0].style.top = "#{@fixbutton_inity}px"
#if @move_lock == "y"
# @fixbutton[0].style.top = "#{@fixbutton_targety} px"
# Check if opened
if (not @opened and targetx > @width/3) or (@opened and targetx > @width*0.9)
@fixbutton_targetx = @fixbutton_initx - @width # Make it opened
else
@fixbutton_targetx = @fixbutton_initx
if (not @console.opened and 0 - targety > @page_height/10) or (@console.opened and 0 - targety > @page_height*0.8)
@fixbutton_targety = @page_height - @fixbutton_inity - 50
else
@fixbutton_targety = @fixbutton_inity
# Stop dragging the fixbutton
stopDrag: ->
@fixbutton.parents().off "mousemove touchmove"
@fixbutton.off "mousemove touchmove"
@fixbutton.css("pointer-events", "")
$(".drag-bg").remove()
if not @fixbutton.hasClass("dragging")
return
@fixbutton.removeClass("dragging")
# Move back to initial position
if @fixbutton_targetx != @fixbutton.offset().left or @fixbutton_targety != @fixbutton.offset().top
# Animate fixbutton
if @move_lock == "y"
top = @fixbutton_targety
left = @fixbutton_initx
if @move_lock == "x"
top = @fixbutton_inity
left = @fixbutton_targetx
@fixbutton.stop().animate {"left": left, "top": top}, 500, "easeOutBack", =>
# Switch back to auto align
if @fixbutton_targetx == @fixbutton_initx # Closed
@fixbutton.css("left", "auto")
else # Opened
@fixbutton.css("left", left)
$(".fixbutton-bg").trigger "mouseout" # Switch fixbutton back to normal status
@stopDragX()
@console.stopDragY()
@move_lock = null
stopDragX: ->
# Animate sidebar and iframe
if @fixbutton_targetx == @fixbutton_initx or @move_lock == "y"
# Closed
targetx = 0
@opened = false
else
# Opened
targetx = @width
if @opened
@onOpened()
else
@when_loaded.done =>
@onOpened()
@opened = true
# Revent sidebar transitions
if @tag
@tag.css("transition", "0.4s ease-out")
@tag.css("transform", "translateX(-#{targetx}px)").one transitionEnd, =>
@tag.css("transition", "")
if not @opened
@container.remove()
@container = null
if @tag
@tag.remove()
@tag = null
# Revert body transformations
@log "stopdrag", "opened:", @opened
if not @opened
@onClosed()
sign: (inner_path, privatekey) ->
@wrapper.displayProgress("sign", "Signing: #{inner_path}...", 0)
@wrapper.ws.cmd "siteSign", {privatekey: privatekey, inner_path: inner_path, update_changed_files: true}, (res) =>
if res == "ok"
@wrapper.displayProgress("sign", "#{inner_path} signed!", 100)
else
@wrapper.displayProgress("sign", "Error signing #{inner_path}", -1)
publish: (inner_path, privatekey) ->
@wrapper.ws.cmd "sitePublish", {privatekey: privatekey, inner_path: inner_path, sign: true, update_changed_files: true}, (res) =>
if res == "ok"
@wrapper.notifications.add "sign", "done", "#{inner_path} Signed and published!", 5000
handleSiteDeleteClick: ->
if @wrapper.site_info.privatekey
question = "Are you sure?<br>This site has a saved private key"
options = ["Forget private key and delete site"]
else
question = "Are you sure?"
options = ["Delete this site", "Blacklist"]
@wrapper.displayConfirm question, options, (confirmed) =>
if confirmed == 1
@tag.find("#button-delete").addClass("loading")
@wrapper.ws.cmd "siteDelete", @wrapper.site_info.address, ->
document.location = $(".fixbutton-bg").attr("href")
else if confirmed == 2
@wrapper.displayPrompt "Blacklist this site", "text", "Delete and Blacklist", "Reason", (reason) =>
@tag.find("#button-delete").addClass("loading")
@wrapper.ws.cmd "siteblockAdd", [@wrapper.site_info.address, reason]
@wrapper.ws.cmd "siteDelete", @wrapper.site_info.address, ->
document.location = $(".fixbutton-bg").attr("href")
onOpened: ->
@log "Opened"
@scrollable()
# Re-calculate height when site admin opened or closed
@tag.find("#checkbox-owned, #checkbox-autodownloadoptional").off("click touchend").on "click touchend", =>
setTimeout (=>
@scrollable()
), 300
# Site limit button
@tag.find("#button-sitelimit").off("click touchend").on "click touchend", =>
@wrapper.ws.cmd "siteSetLimit", $("#input-sitelimit").val(), (res) =>
if res == "ok"
@wrapper.notifications.add "done-sitelimit", "done", "Site storage limit modified!", 5000
@updateHtmlTag()
return false
# Site autodownload limit button
@tag.find("#button-autodownload_bigfile_size_limit").off("click touchend").on "click touchend", =>
@wrapper.ws.cmd "siteSetAutodownloadBigfileLimit", $("#input-autodownload_bigfile_size_limit").val(), (res) =>
if res == "ok"
@wrapper.notifications.add "done-bigfilelimit", "done", "Site bigfile auto download limit modified!", 5000
@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", [], =>
@wrapper.notifications.add "done-dbreload", "done", "Database schema reloaded!", 5000
@updateHtmlTag()
return false
# Database rebuild
@tag.find("#button-dbrebuild").off("click touchend").on "click touchend", =>
@wrapper.notifications.add "done-dbrebuild", "info", "Database rebuilding...."
@wrapper.ws.cmd "dbRebuild", [], =>
@wrapper.notifications.add "done-dbrebuild", "done", "Database rebuilt!", 5000
@updateHtmlTag()
return false
# Update site
@tag.find("#button-update").off("click touchend").on "click touchend", =>
@tag.find("#button-update").addClass("loading")
@wrapper.ws.cmd "siteUpdate", @wrapper.site_info.address, =>
@wrapper.notifications.add "done-updated", "done", "Site updated!", 5000
@tag.find("#button-update").removeClass("loading")
return false
# Pause site
@tag.find("#button-pause").off("click touchend").on "click touchend", =>
@tag.find("#button-pause").addClass("hidden")
@wrapper.ws.cmd "sitePause", @wrapper.site_info.address
return false
# Resume site
@tag.find("#button-resume").off("click touchend").on "click touchend", =>
@tag.find("#button-resume").addClass("hidden")
@wrapper.ws.cmd "siteResume", @wrapper.site_info.address
return false
# Delete site
@tag.find("#button-delete").off("click touchend").on "click touchend", =>
@handleSiteDeleteClick()
return false
# Owned checkbox
@tag.find("#checkbox-owned").off("click touchend").on "click touchend", =>
owned = @tag.find("#checkbox-owned").is(":checked")
@wrapper.ws.cmd "siteSetOwned", [owned], (res_set_owned) =>
@log "Owned", owned
if owned
@wrapper.ws.cmd "siteRecoverPrivatekey", [], (res_recover) =>
if res_recover == "ok"
@wrapper.notifications.add("recover", "done", "Private key recovered from master seed", 5000)
else
@log "Unable to recover private key: #{res_recover.error}"
# Owned auto download checkbox
@tag.find("#checkbox-autodownloadoptional").off("click touchend").on "click touchend", =>
@wrapper.ws.cmd "siteSetAutodownloadoptional", [@tag.find("#checkbox-autodownloadoptional").is(":checked")]
# Change identity button
@tag.find("#button-identity").off("click touchend").on "click touchend", =>
@wrapper.ws.cmd "certSelect"
return false
# Save settings
@tag.find("#button-settings").off("click touchend").on "click touchend", =>
@wrapper.ws.cmd "fileGet", "content.json", (res) =>
data = JSON.parse(res)
data["title"] = $("#settings-title").val()
data["description"] = $("#settings-description").val()
json_raw = unescape(encodeURIComponent(JSON.stringify(data, undefined, '\t')))
@wrapper.ws.cmd "fileWrite", ["content.json", btoa(json_raw), true], (res) =>
if res != "ok" # fileWrite failed
@wrapper.notifications.add "file-write", "error", "File write error: #{res}"
else
@wrapper.notifications.add "file-write", "done", "Site settings saved!", 5000
if @wrapper.site_info.privatekey
@wrapper.ws.cmd "siteSign", {privatekey: "stored", inner_path: "content.json", update_changed_files: true}
@updateHtmlTag()
return false
# Open site directory
@tag.find("#link-directory").off("click touchend").on "click touchend", =>
@wrapper.ws.cmd "serverShowdirectory", ["site", @wrapper.site_info.address]
return false
# Copy site with peers
@tag.find("#link-copypeers").off("click touchend").on "click touchend", (e) =>
copy_text = e.currentTarget.href
handler = (e) =>
e.clipboardData.setData('text/plain', copy_text)
e.preventDefault()
@wrapper.notifications.add "copy", "done", "Site address with peers copied to your clipboard", 5000
document.removeEventListener('copy', handler, true)
document.addEventListener('copy', handler, true)
document.execCommand('copy')
return false
# Sign and publish content.json
$(document).on "click touchend", =>
@tag?.find("#button-sign-publish-menu").removeClass("visible")
@tag?.find(".contents + .flex").removeClass("sign-publish-flex")
@tag.find(".contents-content").off("click touchend").on "click touchend", (e) =>
$("#input-contents").val(e.currentTarget.innerText);
return false;
menu = new Menu(@tag.find("#menu-sign-publish"))
menu.elem.css("margin-top", "-130px") # Open upwards
menu.addItem "Sign", =>
inner_path = @tag.find("#input-contents").val()
@wrapper.ws.cmd "fileRules", {inner_path: inner_path}, (rules) =>
if @wrapper.site_info.auth_address in rules.signers
# ZeroID or other ID provider
@sign(inner_path)
else if @wrapper.site_info.privatekey
# Privatekey stored in users.json
@sign(inner_path, "stored")
else
# Ask the user for privatekey
@wrapper.displayPrompt "Enter your private key:", "password", "Sign", "", (privatekey) => # Prompt the private key
@sign(inner_path, privatekey)
@tag.find(".contents + .flex").removeClass "active"
menu.hide()
menu.addItem "Publish", =>
inner_path = @tag.find("#input-contents").val()
@wrapper.ws.cmd "sitePublish", {"inner_path": inner_path, "sign": false}
@tag.find(".contents + .flex").removeClass "active"
menu.hide()
@tag.find("#menu-sign-publish").off("click touchend").on "click touchend", =>
if window.visible_menu == menu
@tag.find(".contents + .flex").removeClass "active"
menu.hide()
else
@tag.find(".contents + .flex").addClass "active"
@tag.find(".content-wrapper").prop "scrollTop", 10000
menu.show()
return false
$("body").on "click", =>
if @tag
@tag.find(".contents + .flex").removeClass "active"
@tag.find("#button-sign-publish").off("click touchend").on "click touchend", =>
inner_path = @tag.find("#input-contents").val()
@wrapper.ws.cmd "fileRules", {inner_path: inner_path}, (rules) =>
if @wrapper.site_info.auth_address in rules.signers
# ZeroID or other ID provider
@publish(inner_path, null)
else if @wrapper.site_info.privatekey
# Privatekey stored in users.json
@publish(inner_path, "stored")
else
# Ask the user for privatekey
@wrapper.displayPrompt "Enter your private key:", "password", "Sign", "", (privatekey) => # Prompt the private key
@publish(inner_path, privatekey)
return false
# Close
@tag.find(".close").off("click touchend").on "click touchend", (e) =>
@close()
return false
@loadGlobe()
close: ->
@move_lock = "x"
@startDrag()
@stopDrag()
onClosed: ->
$(window).off "resize"
$(window).on "resize", @resized
$(document.body).css("transition", "0.6s ease-in-out").removeClass("body-sidebar").on transitionEnd, (e) =>
if e.target == document.body and not $(document.body).hasClass("body-sidebar") and not $(document.body).hasClass("body-console")
$(document.body).css("height", "auto").css("perspective", "").css("will-change", "").css("transition", "").off transitionEnd
@unloadGlobe()
# We dont need site info anymore
@wrapper.setSiteInfo = @original_set_site_info
loadGlobe: =>
if @tag.find(".globe").hasClass("loading")
setTimeout (=>
if typeof(DAT) == "undefined" # Globe script not loaded, do it first
script_tag = $("<script>")
script_tag.attr("nonce", @wrapper.script_nonce)
script_tag.attr("src", "/uimedia/globe/all.js")
script_tag.on("load", @displayGlobe)
document.head.appendChild(script_tag[0])
else
@displayGlobe()
), 600
displayGlobe: =>
img = new Image();
img.src = "/uimedia/globe/world.jpg";
img.onload = =>
@wrapper.ws.cmd "sidebarGetPeers", [], (globe_data) =>
if @globe
@globe.scene.remove(@globe.points)
@globe.addData( globe_data, {format: 'magnitude', name: "hello", animated: false} )
@globe.createPoints()
@tag?.find(".globe").removeClass("loading")
else if typeof(DAT) != "undefined"
try
@globe = new DAT.Globe( @tag.find(".globe")[0], {"imgDir": "/uimedia/globe/"} )
@globe.addData( globe_data, {format: 'magnitude', name: "hello"} )
@globe.createPoints()
@globe.animate()
catch e
console.log "WebGL error", e
@tag?.find(".globe").addClass("error").text("WebGL not supported")
@tag?.find(".globe").removeClass("loading")
unloadGlobe: =>
if not @globe
return false
@globe.unload()
@globe = null
wrapper = window.wrapper
setTimeout ( ->
window.sidebar = new Sidebar(wrapper)
), 500
window.transitionEnd = 'transitionend webkitTransitionEnd oTransitionEnd otransitionend'

View file

@ -1137,6 +1137,22 @@ window.initScrollable = function () {
return false; return false;
}; };
})(this)); })(this));
this.tag.find("#button-favourite").off("click touched").on("click touched", (function(_this) {
return function() {
_this.tag.find("#button-favourite").addClass("hidden");
_this.tag.find("#button-unfavourite").removeClass("hidden");
_this.wrapper.ws.cmd("siteFavourite", _this.wrapper.site_info.address);
return false;
};
})(this));
this.tag.find("#button-unfavourite").off("click touched").on("click touched", (function(_this) {
return function() {
_this.tag.find("#button-favourite").removeClass("hidden");
_this.tag.find("#button-unfavourite").addClass("hidden");
_this.wrapper.ws.cmd("siteUnfavourite", _this.wrapper.site_info.address);
return false;
};
})(this));
this.tag.find("#button-delete").off("click touchend").on("click touchend", (function(_this) { this.tag.find("#button-delete").off("click touchend").on("click touchend", (function(_this) {
return function() { return function() {
_this.handleSiteDeleteClick(); _this.handleSiteDeleteClick();
@ -1767,4 +1783,4 @@ function morphdom(fromNode, toNode, options) {
module.exports = morphdom; module.exports = morphdom;
},{}]},{},[1])(1) },{}]},{},[1])(1)
}); });

View file

@ -41,7 +41,7 @@ class UiRequestPlugin(object):
from Crypt import CryptConnection from Crypt import CryptConnection
# Memory # Memory
yield "rev%s | " % config.rev yield f'{config.version_full} | '
yield "%s | " % main.file_server.ip_external_list yield "%s | " % main.file_server.ip_external_list
yield "Port: %s | " % main.file_server.port yield "Port: %s | " % main.file_server.port
yield "Network: %s | " % main.file_server.supported_ip_types yield "Network: %s | " % main.file_server.supported_ip_types
@ -579,7 +579,7 @@ class ActionsPlugin:
yield "\n" yield "\n"
yield from self.formatTable( yield from self.formatTable(
["ZeroNet version:", "%s rev%s" % (config.version, config.rev)], ["zeronet-conservancy version:", config.version_full],
["Python:", "%s" % sys.version], ["Python:", "%s" % sys.version],
["Platform:", "%s" % sys.platform], ["Platform:", "%s" % sys.platform],
["Crypt verify lib:", "%s" % CryptBitcoin.lib_verify_best], ["Crypt verify lib:", "%s" % CryptBitcoin.lib_verify_best],

View file

@ -1,222 +0,0 @@
class ConfigStorage extends Class
constructor: (@config) ->
@items = []
@createSections()
@setValues(@config)
setValues: (values) ->
for section in @items
for item in section.items
if not values[item.key]
continue
item.value = @formatValue(values[item.key].value)
item.default = @formatValue(values[item.key].default)
item.pending = values[item.key].pending
values[item.key].item = item
formatValue: (value) ->
if not value
return false
else if typeof(value) == "object"
return value.join("\n")
else if typeof(value) == "number"
return value.toString()
else
return value
deformatValue: (value, type) ->
if type == "object" and typeof(value) == "string"
if not value.length
return value = null
else
return value.split("\n")
if type == "boolean" and not value
return false
else if type == "number"
if typeof(value) == "number"
return value.toString()
else if not value
return "0"
else
return value
else
return value
createSections: ->
# Web Interface
section = @createSection("Web Interface")
section.items.push
key: "open_browser"
title: "Open web browser on ZeroNet startup"
type: "checkbox"
# Network
section = @createSection("Network")
section.items.push
key: "offline"
title: "Offline mode"
type: "checkbox"
description: "Disable network communication."
section.items.push
key: "fileserver_ip_type"
title: "File server network"
type: "select"
options: [
{title: "IPv4", value: "ipv4"}
{title: "IPv6", value: "ipv6"}
{title: "Dual (IPv4 & IPv6)", value: "dual"}
]
description: "Accept incoming peers using IPv4 or IPv6 address. (default: dual)"
section.items.push
key: "fileserver_port"
title: "File server port"
type: "text"
valid_pattern: /[0-9]*/
description: "Other peers will use this port to reach your served sites. (default: randomize)"
section.items.push
key: "ip_external"
title: "File server external ip"
type: "textarea"
placeholder: "Detect automatically"
description: "Your file server is accessible on these ips. (default: detect automatically)"
section.items.push
title: "Tor"
key: "tor"
type: "select"
options: [
{title: "Disable", value: "disable"}
{title: "Enable", value: "enable"}
{title: "Always", value: "always"}
]
description: [
"Disable: Don't connect to peers on Tor network", h("br"),
"Enable: Only use Tor for Tor network peers", h("br"),
"Always: Use Tor for every connections to hide your IP address (slower)"
]
section.items.push
title: "Use Tor bridges"
key: "tor_use_bridges"
type: "checkbox"
description: "Use obfuscated bridge relays to avoid network level Tor block (even slower)"
isHidden: ->
return not Page.server_info.tor_has_meek_bridges
section.items.push
title: "Trackers"
key: "trackers"
type: "textarea"
description: "Discover new peers using these adresses"
section.items.push
title: "Trackers files"
key: "trackers_file"
type: "textarea"
description: "Load additional list of torrent trackers dynamically, from a file"
placeholder: "Eg.: {data_dir}/trackers.json"
value_pos: "fullwidth"
section.items.push
title: "Proxy for tracker connections"
key: "trackers_proxy"
type: "select"
options: [
{title: "Custom", value: ""}
{title: "Tor", value: "tor"}
{title: "Disable", value: "disable"}
]
isHidden: ->
Page.values["tor"] == "always"
section.items.push
title: "Custom socks proxy address for trackers"
key: "trackers_proxy"
type: "text"
placeholder: "Eg.: 127.0.0.1:1080"
value_pos: "fullwidth"
valid_pattern: /.+:[0-9]+/
isHidden: =>
Page.values["trackers_proxy"] in ["tor", "disable"]
# Performance
section = @createSection("Performance")
section.items.push
key: "log_level"
title: "Level of logging to file"
type: "select"
options: [
{title: "Everything", value: "DEBUG"}
{title: "Only important messages", value: "INFO"}
{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
section.items = []
@items.push(section)
return section
window.ConfigStorage = ConfigStorage

View file

@ -1,124 +0,0 @@
class ConfigView extends Class
constructor: () ->
@
render: ->
@config_storage.items.map @renderSection
renderSection: (section) =>
h("div.section", {key: section.title}, [
h("h2", section.title),
h("div.config-items", section.items.map @renderSectionItem)
])
handleResetClick: (e) =>
node = e.currentTarget
config_key = node.attributes.config_key.value
default_value = node.attributes.default_value?.value
Page.cmd "wrapperConfirm", ["Reset #{config_key} value?", "Reset to default"], (res) =>
if (res)
@values[config_key] = default_value
Page.projector.scheduleRender()
renderSectionItem: (item) =>
value_pos = item.value_pos
if item.type == "textarea"
value_pos ?= "fullwidth"
else
value_pos ?= "right"
value_changed = @config_storage.formatValue(@values[item.key]) != item.value
value_default = @config_storage.formatValue(@values[item.key]) == item.default
if item.key in ["open_browser", "fileserver_port"] # Value default for some settings makes no sense
value_default = true
marker_title = "Changed from default value: #{item.default} -> #{@values[item.key]}"
if item.pending
marker_title += " (change pending until client restart)"
if item.isHidden?()
return null
h("div.config-item", {key: item.title, enterAnimation: Animation.slideDown, exitAnimation: Animation.slideUpInout}, [
h("div.title", [
h("h3", item.title),
h("div.description", item.description)
])
h("div.value.value-#{value_pos}",
if item.type == "select"
@renderValueSelect(item)
else if item.type == "checkbox"
@renderValueCheckbox(item)
else if item.type == "textarea"
@renderValueTextarea(item)
else
@renderValueText(item)
h("a.marker", {
href: "#Reset", title: marker_title,
onclick: @handleResetClick, config_key: item.key, default_value: item.default,
classes: {default: value_default, changed: value_changed, visible: not value_default or value_changed or item.pending, pending: item.pending}
}, "\u2022")
)
])
# Values
handleInputChange: (e) =>
node = e.target
config_key = node.attributes.config_key.value
@values[config_key] = node.value
Page.projector.scheduleRender()
handleCheckboxChange: (e) =>
node = e.currentTarget
config_key = node.attributes.config_key.value
value = not node.classList.contains("checked")
@values[config_key] = value
Page.projector.scheduleRender()
renderValueText: (item) =>
value = @values[item.key]
if not value
value = ""
h("input.input-#{item.type}", {type: item.type, config_key: item.key, value: value, placeholder: item.placeholder, oninput: @handleInputChange})
autosizeTextarea: (e) =>
if e.currentTarget
# @handleInputChange(e)
node = e.currentTarget
else
node = e
height_before = node.style.height
if height_before
node.style.height = "0px"
h = node.offsetHeight
scrollh = node.scrollHeight + 20
if scrollh > h
node.style.height = scrollh + "px"
else
node.style.height = height_before
renderValueTextarea: (item) =>
value = @values[item.key]
if not value
value = ""
h("textarea.input-#{item.type}.input-text",{
type: item.type, config_key: item.key, oninput: @handleInputChange, afterCreate: @autosizeTextarea,
updateAnimation: @autosizeTextarea, value: value, placeholder: item.placeholder
})
renderValueCheckbox: (item) =>
if @values[item.key] and @values[item.key] != "False"
checked = true
else
checked = false
h("div.checkbox", {onclick: @handleCheckboxChange, config_key: item.key, classes: {checked: checked}}, h("div.checkbox-skin"))
renderValueSelect: (item) =>
h("select.input-select", {config_key: item.key, oninput: @handleInputChange},
item.options.map (option) =>
h("option", {selected: option.value.toString() == @values[item.key], value: option.value}, option.title)
)
window.ConfigView = ConfigView

View file

@ -1,129 +0,0 @@
window.h = maquette.h
class UiConfig extends ZeroFrame
init: ->
@save_visible = true
@config = null # Setting currently set on the server
@values = null # Entered values on the page
@config_view = new ConfigView()
window.onbeforeunload = =>
if @getValuesChanged().length > 0
return true
else
return null
onOpenWebsocket: =>
@cmd("wrapperSetTitle", "Config - ZeroNet")
@cmd "serverInfo", {}, (server_info) =>
@server_info = server_info
@restart_loading = false
@updateConfig()
updateConfig: (cb) =>
@cmd "configList", [], (res) =>
@config = res
@values = {}
@config_storage = new ConfigStorage(@config)
@config_view.values = @values
@config_view.config_storage = @config_storage
for key, item of res
value = item.value
@values[key] = @config_storage.formatValue(value)
@projector.scheduleRender()
cb?()
createProjector: =>
@projector = maquette.createProjector()
@projector.replace($("#content"), @render)
@projector.replace($("#bottom-save"), @renderBottomSave)
@projector.replace($("#bottom-restart"), @renderBottomRestart)
getValuesChanged: =>
values_changed = []
for key, value of @values
if @config_storage.formatValue(value) != @config_storage.formatValue(@config[key]?.value)
values_changed.push({key: key, value: value})
return values_changed
getValuesPending: =>
values_pending = []
for key, item of @config
if item.pending
values_pending.push(key)
return values_pending
saveValues: (cb) =>
changed_values = @getValuesChanged()
for item, i in changed_values
last = i == changed_values.length - 1
value = @config_storage.deformatValue(item.value, typeof(@config[item.key].default))
default_value = @config_storage.deformatValue(@config[item.key].default, typeof(@config[item.key].default))
value_same_as_default = JSON.stringify(default_value) == JSON.stringify(value)
if @config[item.key].item.valid_pattern and not @config[item.key].item.isHidden?()
match = value.match(@config[item.key].item.valid_pattern)
if not match or match[0] != value
message = "Invalid value of #{@config[item.key].item.title}: #{value} (does not matches #{@config[item.key].item.valid_pattern})"
Page.cmd("wrapperNotification", ["error", message])
cb(false)
break
if value_same_as_default
value = null
@saveValue(item.key, value, if last then cb else null)
saveValue: (key, value, cb) =>
if key == "open_browser"
if value
value = "default_browser"
else
value = "False"
Page.cmd "configSet", [key, value], (res) =>
if res != "ok"
Page.cmd "wrapperNotification", ["error", res.error]
cb?(true)
render: =>
if not @config
return h("div.content")
h("div.content", [
@config_view.render()
])
handleSaveClick: =>
@save_loading = true
@logStart "Save"
@saveValues (success) =>
@save_loading = false
@logEnd "Save"
if success
@updateConfig()
Page.projector.scheduleRender()
return false
renderBottomSave: =>
values_changed = @getValuesChanged()
h("div.bottom.bottom-save", {classes: {visible: values_changed.length}}, h("div.bottom-content", [
h("div.title", "#{values_changed.length} configuration item value changed"),
h("a.button.button-submit.button-save", {href: "#Save", classes: {loading: @save_loading}, onclick: @handleSaveClick}, "Save settings")
]))
handleRestartClick: =>
@restart_loading = true
Page.cmd("serverShutdown", {restart: true})
Page.projector.scheduleRender()
return false
renderBottomRestart: =>
values_pending = @getValuesPending()
values_changed = @getValuesChanged()
h("div.bottom.bottom-restart", {classes: {visible: values_pending.length and not values_changed.length}}, h("div.bottom-content", [
h("div.title", "Some changed settings requires restart"),
h("a.button.button-submit.button-restart", {href: "#Restart", classes: {loading: @restart_loading}, onclick: @handleRestartClick}, "Restart ZeroNet client")
]))
window.Page = new UiConfig()
window.Page.createProjector()

View file

@ -1,23 +0,0 @@
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

@ -1,74 +0,0 @@
# 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

@ -1,8 +0,0 @@
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

@ -1,138 +0,0 @@
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

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

View file

@ -1,85 +0,0 @@
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

@ -17366,7 +17366,10 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
var kw = keywords[word] var kw = keywords[word]
return ret(kw.type, kw.style, word) return ret(kw.type, kw.style, word)
} }
if (word == "async" && stream.match(/^(\s|\/\*.*?\*\/)*[\[\(\w]/, false)) // backported ReDoS fix from
// https://github.com/codemirror/codemirror5/blob/a0854c752a76e4ba9512a9beedb9076f36e4f8f9/mode/javascript/javascript.js#L130C36-L130C36
// https://security.snyk.io/vuln/SNYK-JS-CODEMIRROR-1016937
if (word == "async" && stream.match(/^(\s|\/\*([^*]|\*(?!\/))*?\*\/)*[\[\(\w]/, false))
return ret("async", "keyword", word) return ret("async", "keyword", word)
} }
return ret("variable", "variable", word) return ret("variable", "variable", word)

View file

@ -126,7 +126,10 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
var kw = keywords[word] var kw = keywords[word]
return ret(kw.type, kw.style, word) return ret(kw.type, kw.style, word)
} }
if (word == "async" && stream.match(/^(\s|\/\*.*?\*\/)*[\[\(\w]/, false)) // backported ReDoS fix from
// https://github.com/codemirror/codemirror5/blob/a0854c752a76e4ba9512a9beedb9076f36e4f8f9/mode/javascript/javascript.js#L130C36-L130C36
// https://security.snyk.io/vuln/SNYK-JS-CODEMIRROR-1016937
if (word == "async" && stream.match(/^(\s|\/\*([^*]|\*(?!\/))*?\*\/)*[\[\(\w]/, false))
return ret("async", "keyword", word) return ret("async", "keyword", word)
} }
return ret("variable", "variable", word) return ret("variable", "variable", word)

View file

@ -1,15 +0,0 @@
window.BINARY_EXTENSIONS = [
"3dm", "3ds", "3g2", "3gp", "7z", "a", "aac", "adp", "ai", "aif", "aiff", "alz", "ape", "apk", "appimage", "ar", "arj", "asc", "asf", "au", "avi", "bak",
"baml", "bh", "bin", "bk", "bmp", "btif", "bz2", "bzip2", "cab", "caf", "cgm", "class", "cmx", "cpio", "cr2", "cur", "dat", "dcm", "deb", "dex", "djvu",
"dll", "dmg", "dng", "doc", "docm", "docx", "dot", "dotm", "dra", "DS_Store", "dsk", "dts", "dtshd", "dvb", "dwg", "dxf", "ecelp4800", "ecelp7470",
"ecelp9600", "egg", "eol", "eot", "epub", "exe", "f4v", "fbs", "fh", "fla", "flac", "flatpak", "fli", "flv", "fpx", "fst", "fvt", "g3", "gh", "gif",
"gpg", "graffle", "gz", "gzip", "h261", "h263", "h264", "icns", "ico", "ief", "img", "ipa", "iso", "jar", "jpeg", "jpg", "jpgv", "jpm", "jxr", "key",
"ktx", "lha", "lib", "lvp", "lz", "lzh", "lzma", "lzo", "m3u", "m4a", "m4v", "mar", "mdi", "mht", "mid", "midi", "mj2", "mka", "mkv", "mmr", "mng",
"mobi", "mov", "movie", "mp3", "mp4", "mp4a", "mpeg", "mpg", "mpga", "msgpack", "mxu", "nef", "npx", "numbers", "nupkg", "o", "oga", "ogg", "ogv",
"otf", "pages", "pbm", "pcx", "pdb", "pdf", "pea", "pgm", "pic", "png", "pnm", "pot", "potm", "potx", "ppa", "ppam", "ppm", "pps", "ppsm", "ppsx",
"ppt", "pptm", "pptx", "psd", "pya", "pyc", "pyo", "pyv", "qt", "rar", "ras", "raw", "resources", "rgb", "rip", "rlc", "rmf", "rmvb", "rpm", "rtf",
"rz", "s3m", "s7z", "scpt", "sgi", "shar", "sig", "sil", "sketch", "slk", "smv", "snap", "snk", "so", "stl", "sub", "suo", "swf", "tar", "tbz2", "tbz",
"tga", "tgz", "thmx", "tif", "tiff", "tlz", "ttc", "ttf", "txz", "udf", "uvh", "uvi", "uvm", "uvp", "uvs", "uvu", "viv", "vob", "war", "wav", "wax",
"wbmp", "wdp", "weba", "webm", "webp", "whl", "wim", "wm", "wma", "wmv", "wmx", "woff2", "woff", "wrm", "wvx", "xbm", "xif", "xla", "xlam", "xls",
"xlsb", "xlsm", "xlsx", "xlt", "xltm", "xltx", "xm", "xmind", "xpi", "xpm", "xwd", "xz", "z", "zip", "zipx"
]

View file

@ -1,179 +0,0 @@
class FileEditor extends Class
constructor: (@inner_path) ->
@need_update = true
@on_loaded = new Promise()
@is_loading = false
@content = ""
@node_cm = null
@cm = null
@error = null
@is_loaded = false
@is_modified = false
@is_saving = false
@mode = "Loading"
update: ->
is_required = Page.url_params.get("edit_mode") != "new"
Page.cmd "fileGet", {inner_path: @inner_path, required: is_required}, (res) =>
if res?.error
@error = res.error
@content = res.error
@log "Error loading: #{@error}"
else
if res
@content = res
else
@content = ""
@mode = "Create"
if not @content
@cm.getDoc().clearHistory()
@cm.setValue(@content)
if not @error
@is_loaded = true
Page.projector.scheduleRender()
isModified: =>
return @content != @cm.getValue()
storeCmNode: (node) =>
@node_cm = node
getMode: (inner_path) ->
ext = inner_path.split(".").pop()
types = {
"py": "python",
"json": "application/json",
"js": "javascript",
"coffee": "coffeescript",
"html": "htmlmixed",
"htm": "htmlmixed",
"php": "htmlmixed",
"rs": "rust",
"css": "css",
"md": "markdown",
"xml": "xml",
"svg": "xml"
}
return types[ext]
foldJson: (from, to) =>
@log "foldJson", from, to
# Get open / close token
startToken = '{'
endToken = '}'
prevLine = @cm.getLine(from.line)
if prevLine.lastIndexOf('[') > prevLine.lastIndexOf('{')
startToken = '['
endToken = ']'
# Get json content
internal = @cm.getRange(from, to)
toParse = startToken + internal + endToken
#Get key count
try
parsed = JSON.parse(toParse)
count = Object.keys(parsed).length
catch e
null
return if count then "\u21A4#{count}\u21A6" else "\u2194"
createCodeMirror: ->
mode = @getMode(@inner_path)
@log "Creating CodeMirror", @inner_path, mode
options = {
value: "Loading...",
mode: mode,
lineNumbers: true,
styleActiveLine: true,
matchBrackets: true,
keyMap: "sublime",
theme: "mdn-like",
extraKeys: {"Ctrl-Space": "autocomplete"},
foldGutter: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"]
}
if mode == "application/json"
options.gutters.unshift("CodeMirror-lint-markers")
options.lint = true
options.foldOptions = { widget: @foldJson }
@cm = CodeMirror(@node_cm, options)
@cm.on "changes", (changes) =>
if @is_loaded and not @is_modified
@is_modified = true
Page.projector.scheduleRender()
loadEditor: ->
if not @is_loading
document.getElementsByTagName("head")[0].insertAdjacentHTML(
"beforeend",
"""<link rel="stylesheet" href="codemirror/all.css" />"""
)
script = document.createElement('script')
script.src = "codemirror/all.js"
script.onload = =>
@createCodeMirror()
@on_loaded.resolve()
document.head.appendChild(script)
return @on_loaded
handleSidebarButtonClick: =>
Page.is_sidebar_closed = not Page.is_sidebar_closed
return false
handleSaveClick: =>
num_errors = (mark for mark in Page.file_editor.cm.getAllMarks() when mark.className == "CodeMirror-lint-mark-error").length
if num_errors > 0
Page.cmd "wrapperConfirm", ["<b>Warning:</b> The file looks invalid.", "Save anyway"], @save
else
@save()
return false
save: =>
Page.projector.scheduleRender()
@is_saving = true
Page.cmd "fileWrite", [@inner_path, Text.fileEncode(@cm.getValue())], (res) =>
@is_saving = false
if res.error
Page.cmd "wrapperNotification", ["error", "Error saving #{res.error}"]
else
@is_save_done = true
setTimeout (() =>
@is_save_done = false
Page.projector.scheduleRender()
), 2000
@content = @cm.getValue()
@is_modified = false
if @mode == "Create"
@mode = "Edit"
Page.file_list.need_update = true
Page.projector.scheduleRender()
render: ->
if @need_update
@loadEditor().then =>
@update()
@need_update = false
h("div.editor", {afterCreate: @storeCmNode, classes: {error: @error, loaded: @is_loaded}}, [
h("a.sidebar-button", {href: "#Sidebar", onclick: @handleSidebarButtonClick}, h("span", "\u2039")),
h("div.editor-head", [
if @mode in ["Edit", "Create"]
h("a.save.button",
{href: "#Save", classes: {loading: @is_saving, done: @is_save_done, disabled: not @is_modified}, onclick: @handleSaveClick},
if @is_save_done then "Save: done!" else "Save"
)
h("span.title", @mode, ": ", @inner_path)
]),
if @error
h("div.error-message",
h("h2", "Unable to load the file: #{@error}")
h("a", {href: Page.file_list.getHref(@inner_path)}, "View in browser")
)
])
window.FileEditor = FileEditor

View file

@ -1,194 +0,0 @@
class FileItemList extends Class
constructor: (@inner_path) ->
@items = []
@updating = false
@files_modified = {}
@dirs_modified = {}
@files_added = {}
@dirs_added = {}
@files_optional = {}
@items_by_name = {}
# Update item list
update: (cb) ->
@updating = true
@logStart("Updating dirlist")
Page.cmd "dirList", {inner_path: @inner_path, stats: true}, (res) =>
if res.error
@error = res.error
else
@error = null
pattern_ignore = RegExp("^" + Page.site_info.content?.ignore)
@items.splice(0, @items.length) # Remove all items
@items_by_name = {}
for row in res
row.type = @getFileType(row)
row.inner_path = @inner_path + row.name
if Page.site_info.content?.ignore and row.inner_path.match(pattern_ignore)
row.ignored = true
@items.push(row)
@items_by_name[row.name] = row
@sort()
if Page.site_info?.settings?.own
@updateAddedFiles()
@updateOptionalFiles =>
@updating = false
cb?()
@logEnd("Updating dirlist", @inner_path)
Page.projector.scheduleRender()
@updateModifiedFiles =>
Page.projector.scheduleRender()
updateModifiedFiles: (cb) =>
# Add modified attribute to changed files
Page.cmd "siteListModifiedFiles", [], (res) =>
@files_modified = {}
@dirs_modified = {}
for inner_path in res.modified_files
@files_modified[inner_path] = true
dir_inner_path = ""
dir_parts = inner_path.split("/")
for dir_part in dir_parts[..-2]
if dir_inner_path
dir_inner_path += "/#{dir_part}"
else
dir_inner_path = dir_part
@dirs_modified[dir_inner_path] = true
cb?()
# Update newly added items list since last sign
updateAddedFiles: =>
Page.cmd "fileGet", "content.json", (res) =>
if not res
return false
content = JSON.parse(res)
# Check new files
if not content.files?
return false
@files_added = {}
for file in @items
if file.name == "content.json" or file.is_dir
continue
if not content.files[@inner_path + file.name]
@files_added[@inner_path + file.name] = true
# Check new dirs
@dirs_added = {}
dirs_content = {}
for file_name of Object.assign({}, content.files, content.files_optional)
if not file_name.startsWith(@inner_path)
continue
pattern = new RegExp("#{@inner_path}(.*?)/")
match = file_name.match(pattern)
if not match
continue
dirs_content[match[1]] = true
for file in @items
if not file.is_dir
continue
if not dirs_content[file.name]
@dirs_added[@inner_path + file.name] = true
# Update optional files list
updateOptionalFiles: (cb) =>
Page.cmd "optionalFileList", {filter: ""}, (res) =>
@files_optional = {}
for optional_file in res
@files_optional[optional_file.inner_path] = optional_file
@addOptionalFilesToItems()
cb?()
# Add optional files to item list
addOptionalFilesToItems: =>
is_added = false
for inner_path, optional_file of @files_optional
if optional_file.inner_path.startsWith(@inner_path)
if @getDirectory(optional_file.inner_path) == @inner_path
# Add optional file to list
file_name = @getFileName(optional_file.inner_path)
if not @items_by_name[file_name]
row = {
"name": file_name, "type": "file", "optional_empty": true,
"size": optional_file.size, "is_dir": false, "inner_path": optional_file.inner_path
}
@items.push(row)
@items_by_name[file_name] = row
is_added = true
else
# Add optional dir to list
dir_name = optional_file.inner_path.replace(@inner_path, "").match(/(.*?)\//, "")?[1]
if dir_name and not @items_by_name[dir_name]
row = {
"name": dir_name, "type": "dir", "optional_empty": true,
"size": 0, "is_dir": true, "inner_path": optional_file.inner_path
}
@items.push(row)
@items_by_name[dir_name] = row
is_added = true
if is_added
@sort()
getFileType: (file) =>
if file.is_dir
return "dir"
else
return "unknown"
getDirectory: (inner_path) ->
if inner_path.indexOf("/") != -1
return inner_path.replace(/^(.*\/)(.*?)$/, "$1")
else
return ""
getFileName: (inner_path) ->
return inner_path.replace(/^(.*\/)(.*?)$/, "$2")
isModified: (inner_path) =>
return @files_modified[inner_path] or @dirs_modified[inner_path]
isAdded: (inner_path) =>
return @files_added[inner_path] or @dirs_added[inner_path]
hasPermissionDelete: (file) =>
if file.type in ["dir", "parent"]
return false
if file.inner_path == "content.json"
return false
optional_info = @getOptionalInfo(file.inner_path)
if optional_info and optional_info.downloaded_percent > 0
return true
else
return Page.site_info?.settings?.own
getOptionalInfo: (inner_path) =>
return @files_optional[inner_path]
sort: =>
@items.sort (a, b) ->
return (b.is_dir - a.is_dir) || a.name.localeCompare(b.name)
window.FileItemList = FileItemList

View file

@ -1,268 +0,0 @@
class FileList extends Class
constructor: (@site, @inner_path, @is_owner=false) ->
@need_update = true
@error = null
@url_root = "/list/" + @site + "/"
if @inner_path
@inner_path += "/"
@url_root += @inner_path
@log("inited", @url_root)
@item_list = new FileItemList(@inner_path)
@item_list.items = @item_list.items
@menu_create = new Menu()
@select_action = null
@selected = {}
@selected_items_num = 0
@selected_items_size = 0
@selected_optional_empty_num = 0
isSelectedAll: ->
false
update: =>
@item_list.update =>
document.body.classList.add("loaded")
getHref: (inner_path) =>
return "/" + @site + "/" + inner_path
getListHref: (inner_path) =>
return "/list/" + @site + "/" + inner_path
getEditHref: (inner_path, mode=null) =>
href = @url_root + "?file=" + inner_path
if mode
href += "&edit_mode=#{mode}"
return href
checkSelectedItems: =>
@selected_items_num = 0
@selected_items_size = 0
@selected_optional_empty_num = 0
for item in @item_list.items
if @selected[item.inner_path]
@selected_items_num += 1
@selected_items_size += item.size
optional_info = @item_list.getOptionalInfo(item.inner_path)
if optional_info and not optional_info.downloaded_percent > 0
@selected_optional_empty_num += 1
handleMenuCreateClick: =>
@menu_create.items = []
@menu_create.items.push ["File", @handleNewFileClick]
@menu_create.items.push ["Directory", @handleNewDirectoryClick]
@menu_create.toggle()
return false
handleNewFileClick: =>
Page.cmd "wrapperPrompt", "New file name:", (file_name) =>
window.top.location.href = @getEditHref(@inner_path + file_name, "new")
return false
handleNewDirectoryClick: =>
Page.cmd "wrapperPrompt", "New directory name:", (res) =>
alert("directory name #{res}")
return false
handleSelectClick: (e) =>
return false
handleSelectEnd: (e) =>
document.body.removeEventListener('mouseup', @handleSelectEnd)
@select_action = null
handleSelectMousedown: (e) =>
inner_path = e.currentTarget.attributes.inner_path.value
if @selected[inner_path]
delete @selected[inner_path]
@select_action = "deselect"
else
@selected[inner_path] = true
@select_action = "select"
@checkSelectedItems()
document.body.addEventListener('mouseup', @handleSelectEnd)
e.stopPropagation()
Page.projector.scheduleRender()
return false
handleRowMouseenter: (e) =>
if e.buttons and @select_action
inner_path = e.target.attributes.inner_path.value
if @select_action == "select"
@selected[inner_path] = true
else
delete @selected[inner_path]
@checkSelectedItems()
Page.projector.scheduleRender()
return false
handleSelectbarCancel: =>
@selected = {}
@checkSelectedItems()
Page.projector.scheduleRender()
return false
handleSelectbarDelete: (e, remove_optional=false) =>
for inner_path of @selected
optional_info = @item_list.getOptionalInfo(inner_path)
delete @selected[inner_path]
if optional_info and not remove_optional
Page.cmd "optionalFileDelete", inner_path
else
Page.cmd "fileDelete", inner_path
@need_update = true
Page.projector.scheduleRender()
@checkSelectedItems()
return false
handleSelectbarRemoveOptional: (e) =>
return @handleSelectbarDelete(e, true)
renderSelectbar: =>
h("div.selectbar", {classes: {visible: @selected_items_num > 0}}, [
"Selected:",
h("span.info", [
h("span.num", "#{@selected_items_num} files"),
h("span.size", "(#{Text.formatSize(@selected_items_size)})"),
])
h("div.actions", [
if @selected_optional_empty_num > 0
h("a.action.delete.remove_optional", {href: "#", onclick: @handleSelectbarRemoveOptional}, "Delete and remove optional")
else
h("a.action.delete", {href: "#", onclick: @handleSelectbarDelete}, "Delete")
])
h("a.cancel.link", {href: "#", onclick: @handleSelectbarCancel}, "Cancel")
])
renderHead: =>
parent_links = []
inner_path_parent = ""
for parent_dir in @inner_path.split("/")
if not parent_dir
continue
if inner_path_parent
inner_path_parent += "/"
inner_path_parent += "#{parent_dir}"
parent_links.push(
[" / ", h("a", {href: @getListHref(inner_path_parent)}, parent_dir)]
)
return h("div.tr.thead", h("div.td.full",
h("a", {href: @getListHref("")}, "root"),
parent_links
))
renderItemCheckbox: (item) =>
if not @item_list.hasPermissionDelete(item)
return [" "]
return h("a.checkbox-outer", {
href: "#Select",
onmousedown: @handleSelectMousedown,
onclick: @handleSelectClick,
inner_path: item.inner_path
}, h("span.checkbox"))
renderItem: (item) =>
if item.type == "parent"
href = @url_root.replace(/^(.*)\/.{2,255}?$/, "$1/")
else if item.type == "dir"
href = @url_root + item.name
else
href = @url_root.replace(/^\/list\//, "/") + item.name
inner_path = @inner_path + item.name
href_edit = @getEditHref(inner_path)
is_dir = item.type in ["dir", "parent"]
ext = item.name.split(".").pop()
is_editing = inner_path == Page.file_editor?.inner_path
is_editable = not is_dir and item.size < 1024 * 1024 and ext not in window.BINARY_EXTENSIONS
is_modified = @item_list.isModified(inner_path)
is_added = @item_list.isAdded(inner_path)
optional_info = @item_list.getOptionalInfo(inner_path)
style = ""
title = ""
if optional_info
downloaded_percent = optional_info.downloaded_percent
if not downloaded_percent
downloaded_percent = 0
style += "background: linear-gradient(90deg, #fff6dd, #{downloaded_percent}%, white, #{downloaded_percent}%, white);"
is_added = false
if item.ignored
is_added = false
if is_modified then title += " (modified)"
if is_added then title += " (new)"
if optional_info or item.optional_empty then title += " (optional)"
if item.ignored then title += " (ignored from content.json)"
classes = {
"type-#{item.type}": true, editing: is_editing, nobuttons: not is_editable, selected: @selected[inner_path],
modified: is_modified, added: is_added, ignored: item.ignored, optional: optional_info, optional_empty: item.optional_empty
}
h("div.tr", {key: item.name, classes: classes, style: style, onmouseenter: @handleRowMouseenter, inner_path: inner_path}, [
h("div.td.pre", {title: title},
@renderItemCheckbox(item)
),
h("div.td.name", h("a.link", {href: href}, item.name))
h("div.td.buttons", if is_editable then h("a.edit", {href: href_edit}, if Page.site_info.settings.own then "Edit" else "View"))
h("div.td.size", if is_dir then "[DIR]" else Text.formatSize(item.size))
])
renderItems: =>
return [
if @item_list.error and not @item_list.items.length and not @item_list.updating then [
h("div.tr", {key: "error"}, h("div.td.full.error", @item_list.error))
],
if @inner_path then @renderItem({"name": "..", type: "parent", size: 0})
@item_list.items.map @renderItem
]
renderFoot: =>
files = (item for item in @item_list.items when item.type not in ["parent", "dir"])
dirs = (item for item in @item_list.items when item.type == "dir")
if files.length
total_size = (item.size for file in files).reduce (a, b) -> a + b
else
total_size = 0
foot_text = "Total: "
foot_text += "#{dirs.length} dir, #{files.length} file in #{Text.formatSize(total_size)}"
return [
if dirs.length or files.length or Page.site_info?.settings?.own
h("div.tr.foot-info.foot", h("div.td.full", [
if @item_list.updating
"Updating file list..."
else
if dirs.length or files.length then foot_text
if Page.site_info?.settings?.own
h("div.create", [
h("a.link", {href: "#Create+new+file", onclick: @handleNewFileClick}, "+ New")
@menu_create.render()
])
]))
]
render: =>
if @need_update
@update()
@need_update = false
if not @item_list.items
return []
return h("div.files", [
@renderSelectbar(),
@renderHead(),
h("div.tbody", @renderItems()),
@renderFoot()
])
window.FileList = FileList

View file

@ -1,79 +0,0 @@
window.h = maquette.h
class UiFileManager extends ZeroFrame
init: ->
@url_params = new URLSearchParams(window.location.search)
@list_site = @url_params.get("site")
@list_address = @url_params.get("address")
@list_inner_path = @url_params.get("inner_path")
@editor_inner_path = @url_params.get("file")
@file_list = new FileList(@list_site, @list_inner_path)
@site_info = null
@server_info = null
@is_sidebar_closed = false
if @editor_inner_path
@file_editor = new FileEditor(@editor_inner_path)
window.onbeforeunload = =>
if @file_editor?.isModified()
return true
else
return null
window.onresize = =>
@checkBodyWidth()
@checkBodyWidth()
@cmd("wrapperSetViewport", "width=device-width, initial-scale=0.8")
@cmd "serverInfo", {}, (server_info) =>
@server_info = server_info
@cmd "siteInfo", {}, (site_info) =>
@cmd("wrapperSetTitle", "List: /#{@list_inner_path} - #{site_info.content.title} - ZeroNet")
@site_info = site_info
if @file_editor then @file_editor.on_loaded.then =>
@file_editor.cm.setOption("readOnly", not site_info.settings.own)
@file_editor.mode = if site_info.settings.own then "Edit" else "View"
@projector.scheduleRender()
checkBodyWidth: =>
if not @file_editor
return false
if document.body.offsetWidth < 960 and not @is_sidebar_closed
@is_sidebar_closed = true
@projector?.scheduleRender()
else if document.body.offsetWidth > 960 and @is_sidebar_closed
@is_sidebar_closed = false
@projector?.scheduleRender()
onRequest: (cmd, message) =>
if cmd == "setSiteInfo"
@site_info = message
RateLimitCb 1000, (cb_done) =>
@file_list.update(cb_done)
@projector.scheduleRender()
else if cmd == "setServerInfo"
@server_info = message
@projector.scheduleRender()
else
@log "Unknown incoming message:", cmd
createProjector: =>
@projector = maquette.createProjector()
@projector.replace($("#content"), @render)
render: =>
return h("div.content#content", [
h("div.manager", {classes: {editing: @file_editor, sidebar_closed: @is_sidebar_closed}}, [
@file_list.render(),
if @file_editor then @file_editor.render()
])
])
window.Page = new UiFileManager()
window.Page.createProjector()

View file

@ -1,138 +0,0 @@
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

@ -1,23 +0,0 @@
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

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

View file

@ -1,26 +0,0 @@
class ItemList
constructor: (@item_class, @key) ->
@items = []
@items_bykey = {}
sync: (rows, item_class, key) ->
@items.splice(0, @items.length) # Empty items
for row in rows
current_obj = @items_bykey[row[@key]]
if current_obj
current_obj.row = row
@items.push current_obj
else
item = new @item_class(row, @)
@items_bykey[row[@key]] = item
@items.push item
deleteItem: (item) ->
index = @items.indexOf(item)
if index > -1
@items.splice(index, 1)
else
console.log "Can't delete item", item
delete @items_bykey[item.row[@key]]
window.ItemList = ItemList

View file

@ -1,110 +0,0 @@
class Menu
constructor: ->
@visible = false
@items = []
@node = null
@height = 0
@direction = "bottom"
show: =>
window.visible_menu?.hide()
@visible = true
window.visible_menu = @
@direction = @getDirection()
hide: =>
@visible = false
toggle: =>
if @visible
@hide()
else
@show()
Page.projector.scheduleRender()
addItem: (title, cb, selected=false) ->
@items.push([title, cb, selected])
storeNode: (node) =>
@node = node
# Animate visible
if @visible
node.className = node.className.replace("visible", "")
setTimeout (=>
node.className += " visible"
node.attributes.style.value = @getStyle()
), 20
node.style.maxHeight = "none"
@height = node.offsetHeight
node.style.maxHeight = "0px"
@direction = @getDirection()
getDirection: =>
if @node and @node.parentNode.getBoundingClientRect().top + @height + 60 > document.body.clientHeight and @node.parentNode.getBoundingClientRect().top - @height > 0
return "top"
else
return "bottom"
handleClick: (e) =>
keep_menu = false
for item in @items
[title, cb, selected] = item
if title == e.currentTarget.textContent or e.currentTarget["data-title"] == title
keep_menu = cb?(item)
break
if keep_menu != true and cb != null
@hide()
return false
renderItem: (item) =>
[title, cb, selected] = item
if typeof(selected) == "function"
selected = selected()
if title == "---"
return h("div.menu-item-separator", {key: Time.timestamp()})
else
if cb == null
href = undefined
onclick = @handleClick
else if typeof(cb) == "string" # Url
href = cb
onclick = true
else # Callback
href = "#"+title
onclick = @handleClick
classes = {
"selected": selected,
"noaction": (cb == null)
}
return h("a.menu-item", {href: href, onclick: onclick, "data-title": title, key: title, classes: classes}, title)
getStyle: =>
if @visible
max_height = @height
else
max_height = 0
style = "max-height: #{max_height}px"
if @direction == "top"
style += ";margin-top: #{0 - @height - 50}px"
else
style += ";margin-top: 0px"
return style
render: (class_name="") =>
if @visible or @node
h("div.menu#{class_name}", {classes: {"visible": @visible}, style: @getStyle(), afterCreate: @storeNode}, @items.map(@renderItem))
window.Menu = Menu
# Hide menu on outside click
document.body.addEventListener "mouseup", (e) ->
if not window.visible_menu or not window.visible_menu.node
return false
menu_node = window.visible_menu.node
menu_parents = [menu_node, menu_node.parentNode]
if e.target.parentNode not in menu_parents and e.target.parentNode.parentNode not in menu_parents
window.visible_menu.hide()
Page.projector.scheduleRender()

View file

@ -1,74 +0,0 @@
# 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

@ -1,9 +0,0 @@
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

@ -1,62 +0,0 @@
last_time = {}
calling = {}
calling_iterval = {}
call_after_interval = {}
# Rate limit function call and don't allow to run in parallel (until callback is called)
window.RateLimitCb = (interval, fn, args=[]) ->
cb = -> # Callback when function finished
left = interval - (Date.now() - last_time[fn]) # Time life until next call
# console.log "CB, left", left, "Calling:", calling[fn]
if left <= 0 # No time left from rate limit interval
delete last_time[fn]
if calling[fn] # Function called within interval
RateLimitCb(interval, fn, calling[fn])
delete calling[fn]
else # Time left from rate limit interval
setTimeout (->
delete last_time[fn]
if calling[fn] # Function called within interval
RateLimitCb(interval, fn, calling[fn])
delete calling[fn]
), left
if last_time[fn] # Function called within interval
calling[fn] = args # Schedule call and update arguments
else # Not called within interval, call instantly
last_time[fn] = Date.now()
fn.apply(this, [cb, args...])
window.RateLimit = (interval, fn) ->
if calling_iterval[fn] > interval
clearInterval calling[fn]
delete calling[fn]
if not calling[fn]
call_after_interval[fn] = false
fn() # First call is not delayed
calling_iterval[fn] = interval
calling[fn] = setTimeout (->
if call_after_interval[fn]
fn()
delete calling[fn]
delete call_after_interval[fn]
), interval
else # Called within iterval, delay the call
call_after_interval[fn] = true
###
window.s = Date.now()
window.load = (done, num) ->
console.log "Loading #{num}...", Date.now()-window.s
setTimeout (-> done()), 1000
RateLimit 500, window.load, [0] # Called instantly
RateLimit 500, window.load, [1]
setTimeout (-> RateLimit 500, window.load, [300]), 300
setTimeout (-> RateLimit 500, window.load, [600]), 600 # Called after 1000ms
setTimeout (-> RateLimit 500, window.load, [1000]), 1000
setTimeout (-> RateLimit 500, window.load, [1200]), 1200 # Called after 2000ms
setTimeout (-> RateLimit 500, window.load, [3000]), 3000 # Called after 3000ms
###

View file

@ -1,147 +0,0 @@
class Text
toColor: (text, saturation=30, lightness=50) ->
hash = 0
for i in [0..text.length-1]
hash += text.charCodeAt(i)*i
hash = hash % 1777
return "hsl(" + (hash % 360) + ",#{saturation}%,#{lightness}%)";
renderMarked: (text, options={}) ->
options["gfm"] = true
options["breaks"] = true
options["sanitize"] = true
options["renderer"] = marked_renderer
text = marked(text, options)
return @fixHtmlLinks text
emailLinks: (text) ->
return text.replace(/([a-zA-Z0-9]+)@zeroid.bit/g, "<a href='?to=$1' onclick='return Page.message_create.show(\"$1\")'>$1@zeroid.bit</a>")
# Convert zeronet html links to relaitve
fixHtmlLinks: (text) ->
if window.is_proxy
return text.replace(/href="http:\/\/(127.0.0.1|localhost):43110/g, 'href="http://zero')
else
return text.replace(/href="http:\/\/(127.0.0.1|localhost):43110/g, 'href="')
# Convert a single link to relative
fixLink: (link) ->
if window.is_proxy
back = link.replace(/http:\/\/(127.0.0.1|localhost):43110/, 'http://zero')
return back.replace(/http:\/\/zero\/([^\/]+\.bit)/, "http://$1") # Domain links
else
return link.replace(/http:\/\/(127.0.0.1|localhost):43110/, '')
toUrl: (text) ->
return text.replace(/[^A-Za-z0-9]/g, "+").replace(/[+]+/g, "+").replace(/[+]+$/, "")
getSiteUrl: (address) ->
if window.is_proxy
if "." in address # Domain
return "http://"+address+"/"
else
return "http://zero/"+address+"/"
else
return "/"+address+"/"
fixReply: (text) ->
return text.replace(/(>.*\n)([^\n>])/gm, "$1\n$2")
toBitcoinAddress: (text) ->
return text.replace(/[^A-Za-z0-9]/g, "")
jsonEncode: (obj) ->
return unescape(encodeURIComponent(JSON.stringify(obj)))
jsonDecode: (obj) ->
return JSON.parse(decodeURIComponent(escape(obj)))
fileEncode: (obj) ->
if typeof(obj) == "string"
return btoa(unescape(encodeURIComponent(obj)))
else
return btoa(unescape(encodeURIComponent(JSON.stringify(obj, undefined, '\t'))))
utf8Encode: (s) ->
return unescape(encodeURIComponent(s))
utf8Decode: (s) ->
return decodeURIComponent(escape(s))
distance: (s1, s2) ->
s1 = s1.toLocaleLowerCase()
s2 = s2.toLocaleLowerCase()
next_find_i = 0
next_find = s2[0]
match = true
extra_parts = {}
for char in s1
if char != next_find
if extra_parts[next_find_i]
extra_parts[next_find_i] += char
else
extra_parts[next_find_i] = char
else
next_find_i++
next_find = s2[next_find_i]
if extra_parts[next_find_i]
extra_parts[next_find_i] = "" # Extra chars on the end doesnt matter
extra_parts = (val for key, val of extra_parts)
if next_find_i >= s2.length
return extra_parts.length + extra_parts.join("").length
else
return false
parseQuery: (query) ->
params = {}
parts = query.split('&')
for part in parts
[key, val] = part.split("=")
if val
params[decodeURIComponent(key)] = decodeURIComponent(val)
else
params["url"] = decodeURIComponent(key)
return params
encodeQuery: (params) ->
back = []
if params.url
back.push(params.url)
for key, val of params
if not val or key == "url"
continue
back.push("#{encodeURIComponent(key)}=#{encodeURIComponent(val)}")
return back.join("&")
highlight: (text, search) ->
if not text
return [""]
parts = text.split(RegExp(search, "i"))
back = []
for part, i in parts
back.push(part)
if i < parts.length-1
back.push(h("span.highlight", {key: i}, search))
return back
formatSize: (size) ->
if isNaN(parseInt(size))
return ""
size_mb = size/1024/1024
if size_mb >= 1000
return (size_mb/1024).toFixed(1)+" GB"
else if size_mb >= 100
return size_mb.toFixed(0)+" MB"
else if size/1024 >= 1000
return size_mb.toFixed(2)+" MB"
else
return (parseInt(size)/1024).toFixed(2)+" KB"
window.is_proxy = (document.location.host == "zero" or window.location.pathname == "/")
window.Text = new Text()

View file

@ -1,59 +0,0 @@
class Time
since: (timestamp) ->
now = +(new Date)/1000
if timestamp > 1000000000000 # In ms
timestamp = timestamp/1000
secs = now - timestamp
if secs < 60
back = "Just now"
else if secs < 60*60
minutes = Math.round(secs/60)
back = "" + minutes + " minutes ago"
else if secs < 60*60*24
back = "#{Math.round(secs/60/60)} hours ago"
else if secs < 60*60*24*3
back = "#{Math.round(secs/60/60/24)} days ago"
else
back = "on "+@date(timestamp)
back = back.replace(/^1 ([a-z]+)s/, "1 $1") # 1 days ago fix
return back
dateIso: (timestamp=null) ->
if not timestamp
timestamp = window.Time.timestamp()
if timestamp > 1000000000000 # In ms
timestamp = timestamp/1000
tzoffset = (new Date()).getTimezoneOffset() * 60
return (new Date((timestamp - tzoffset) * 1000)).toISOString().split("T")[0]
date: (timestamp=null, format="short") ->
if not timestamp
timestamp = window.Time.timestamp()
if timestamp > 1000000000000 # In ms
timestamp = timestamp/1000
parts = (new Date(timestamp * 1000)).toString().split(" ")
if format == "short"
display = parts.slice(1, 4)
else if format == "day"
display = parts.slice(1, 3)
else if format == "month"
display = [parts[1], parts[3]]
else if format == "long"
display = parts.slice(1, 5)
return display.join(" ").replace(/( [0-9]{4})/, ",$1")
weekDay: (timestamp) ->
if timestamp > 1000000000000 # In ms
timestamp = timestamp/1000
return ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"][ (new Date(timestamp * 1000)).getDay() ]
timestamp: (date="") ->
if date == "now" or date == ""
return parseInt(+(new Date)/1000)
else
return parseInt(Date.parse(date)/1000)
window.Time = new Time

View file

@ -1,85 +0,0 @@
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

@ -126,96 +126,3 @@ class UiWebsocketPlugin(object):
plugin_manager.saveConfig() plugin_manager.saveConfig()
return "ok" 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

@ -1,132 +0,0 @@
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

@ -1,71 +0,0 @@
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()

View file

@ -1,23 +0,0 @@
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

@ -1,74 +0,0 @@
# 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

@ -1,8 +0,0 @@
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

@ -1,138 +0,0 @@
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

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

View file

@ -1,85 +0,0 @@
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

@ -62,8 +62,8 @@ class ConfigPlugin(object):
def createArguments(self): def createArguments(self):
group = self.parser.add_argument_group("Zeroname plugin") group = self.parser.add_argument_group("Zeroname plugin")
group.add_argument( group.add_argument(
"--bit_resolver", help="ZeroNet site to resolve .bit domains", "--bit-resolver", help="ZeroNet site to resolve .bit domains (deprecated)",
default="1Name2NXVi1RDPDgf5617UoW7xA6YrhM9F", metavar="address" default="1GnACKctkJrGWHTqxk9T9zXo2bLQc2PDnF", metavar="address"
) )
return super(ConfigPlugin, self).createArguments() return super(ConfigPlugin, self).createArguments()

View file

@ -12,7 +12,7 @@ class BootstrapperDb(Db.Db):
def __init__(self): def __init__(self):
self.version = 7 self.version = 7
self.hash_ids = {} # hash -> id cache self.hash_ids = {} # hash -> id cache
super(BootstrapperDb, self).__init__({"db_name": "Bootstrapper"}, "%s/bootstrapper.db" % config.data_dir) super(BootstrapperDb, self).__init__({"db_name": "Bootstrapper"}, config.start_dir / 'bootstrapper.db')
self.foreign_keys = True self.foreign_keys = True
self.checkTables() self.checkTables()
self.updateHashCache() self.updateHashCache()

View file

@ -1,153 +0,0 @@
import logging, json, os, re, sys, time
import gevent
from Plugin import PluginManager
from Config import config
from util import Http
from Debug import Debug
allow_reload = False # No reload supported
log = logging.getLogger("DnschainPlugin")
@PluginManager.registerTo("SiteManager")
class SiteManagerPlugin(object):
dns_cache_path = "%s/dns_cache.json" % config.data_dir
dns_cache = None
# Checks if its a valid address
def isAddress(self, address):
if self.isDomain(address):
return True
else:
return super(SiteManagerPlugin, self).isAddress(address)
# Return: True if the address is domain
def isDomain(self, address):
return re.match(r"(.*?)([A-Za-z0-9_-]+\.[A-Za-z0-9]+)$", address)
# Load dns entries from data/dns_cache.json
def loadDnsCache(self):
if os.path.isfile(self.dns_cache_path):
self.dns_cache = json.load(open(self.dns_cache_path))
else:
self.dns_cache = {}
log.debug("Loaded dns cache, entries: %s" % len(self.dns_cache))
# Save dns entries to data/dns_cache.json
def saveDnsCache(self):
json.dump(self.dns_cache, open(self.dns_cache_path, "wb"), indent=2)
# Resolve domain using dnschain.net
# Return: The address or None
def resolveDomainDnschainNet(self, domain):
try:
match = self.isDomain(domain)
sub_domain = match.group(1).strip(".")
top_domain = match.group(2)
if not sub_domain: sub_domain = "@"
address = None
with gevent.Timeout(5, Exception("Timeout: 5s")):
res = Http.get("https://api.dnschain.net/v1/namecoin/key/%s" % top_domain).read()
data = json.loads(res)["data"]["value"]
if "zeronet" in data:
for key, val in data["zeronet"].items():
self.dns_cache[key+"."+top_domain] = [val, time.time()+60*60*5] # Cache for 5 hours
self.saveDnsCache()
return data["zeronet"].get(sub_domain)
# Not found
return address
except Exception as err:
log.debug("Dnschain.net %s resolve error: %s" % (domain, Debug.formatException(err)))
# Resolve domain using dnschain.info
# Return: The address or None
def resolveDomainDnschainInfo(self, domain):
try:
match = self.isDomain(domain)
sub_domain = match.group(1).strip(".")
top_domain = match.group(2)
if not sub_domain: sub_domain = "@"
address = None
with gevent.Timeout(5, Exception("Timeout: 5s")):
res = Http.get("https://dnschain.info/bit/d/%s" % re.sub(r"\.bit$", "", top_domain)).read()
data = json.loads(res)["value"]
for key, val in data["zeronet"].items():
self.dns_cache[key+"."+top_domain] = [val, time.time()+60*60*5] # Cache for 5 hours
self.saveDnsCache()
return data["zeronet"].get(sub_domain)
# Not found
return address
except Exception as err:
log.debug("Dnschain.info %s resolve error: %s" % (domain, Debug.formatException(err)))
# Resolve domain
# Return: The address or None
def resolveDomain(self, domain):
domain = domain.lower()
if self.dns_cache == None:
self.loadDnsCache()
if domain.count(".") < 2: # Its a topleved request, prepend @. to it
domain = "@."+domain
domain_details = self.dns_cache.get(domain)
if domain_details and time.time() < domain_details[1]: # Found in cache and its not expired
return domain_details[0]
else:
# Resovle dns using dnschain
thread_dnschain_info = gevent.spawn(self.resolveDomainDnschainInfo, domain)
thread_dnschain_net = gevent.spawn(self.resolveDomainDnschainNet, domain)
gevent.joinall([thread_dnschain_net, thread_dnschain_info]) # Wait for finish
if thread_dnschain_info.value and thread_dnschain_net.value: # Booth successfull
if thread_dnschain_info.value == thread_dnschain_net.value: # Same returned value
return thread_dnschain_info.value
else:
log.error("Dns %s missmatch: %s != %s" % (domain, thread_dnschain_info.value, thread_dnschain_net.value))
# Problem during resolve
if domain_details: # Resolve failed, but we have it in the cache
domain_details[1] = time.time()+60*60 # Dont try again for 1 hour
return domain_details[0]
else: # Not found in cache
self.dns_cache[domain] = [None, time.time()+60] # Don't check again for 1 min
return None
# Return or create site and start download site files
# Return: Site or None if dns resolve failed
def need(self, address, all_file=True):
if self.isDomain(address): # Its looks like a domain
address_resolved = self.resolveDomain(address)
if address_resolved:
address = address_resolved
else:
return None
return super(SiteManagerPlugin, self).need(address, all_file)
# Return: Site object or None if not found
def get(self, address):
if self.sites == None: # Not loaded yet
self.load()
if self.isDomain(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 = self.sites.get(address)
return site

View file

@ -1,34 +0,0 @@
import re
from Plugin import PluginManager
@PluginManager.registerTo("UiRequest")
class UiRequestPlugin(object):
def __init__(self, server = None):
from Site import SiteManager
self.site_manager = SiteManager.site_manager
super(UiRequestPlugin, self).__init__(server)
# Media request
def actionSiteMedia(self, path):
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) # Get the wrapper frame output
# Is mediarequest allowed from that referer
def isMediaRequestAllowed(self, site_address, referer):
referer_path = re.sub("http[s]{0,1}://.*?/", "/", referer).replace("/media", "") # Remove site address
referer_site_address = re.match(r"/(?P<address>[A-Za-z0-9\.-]+)(?P<inner_path>/.*|$)", referer_path).group("address")
if referer_site_address == site_address: # Referer site address as simple address
return True
elif self.site_manager.resolveDomain(referer_site_address) == site_address: # Referer site address as dns
return True
else: # Invalid referer
return False

View file

@ -1,3 +0,0 @@
# This plugin is experimental, if you really want to enable uncomment the following lines:
# import DnschainPlugin
# import SiteManagerPlugin

View file

@ -16,7 +16,7 @@ def importPluginnedClasses():
from User import UserManager from User import UserManager
try: try:
local_master_addresses = set(json.load(open("%s/users.json" % config.data_dir)).keys()) # Users in users.json local_master_addresses = set(json.load((config.private_dir / 'users.json').open()).keys()) # Users in users.json
except Exception as err: except Exception as err:
local_master_addresses = set() local_master_addresses = set()
@ -27,6 +27,9 @@ class UiRequestPlugin(object):
self.user_manager = UserManager.user_manager self.user_manager = UserManager.user_manager
super(UiRequestPlugin, self).__init__(*args, **kwargs) super(UiRequestPlugin, self).__init__(*args, **kwargs)
def parsePath(self, path):
return super(UiRequestPlugin, self).parsePath(path)
# Create new user and inject user welcome message if necessary # Create new user and inject user welcome message if necessary
# Return: Html body also containing the injection # Return: Html body also containing the injection
def actionWrapper(self, path, extra_headers=None): def actionWrapper(self, path, extra_headers=None):
@ -269,7 +272,7 @@ class UiWebsocketPlugin(object):
class ConfigPlugin(object): class ConfigPlugin(object):
def createArguments(self): def createArguments(self):
group = self.parser.add_argument_group("Multiuser plugin") group = self.parser.add_argument_group("Multiuser plugin")
group.add_argument('--multiuser_local', help="Enable unsafe Ui functions and write users to disk", action='store_true') group.add_argument('--multiuser-local', help="Enable unsafe Ui functions and write users to disk", action='store_true')
group.add_argument('--multiuser_no_new_sites', help="Denies adding new sites by normal users", action='store_true') group.add_argument('--multiuser-no-new-sites', help="Denies adding new sites by normal users", action='store_true')
return super(ConfigPlugin, self).createArguments() return super(ConfigPlugin, self).createArguments()

View file

@ -8,7 +8,8 @@ from User import UserManager
class TestMultiuser: class TestMultiuser:
def testMemorySave(self, user): def testMemorySave(self, user):
# It should not write users to disk # It should not write users to disk
users_before = open("%s/users.json" % config.data_dir).read() users_json = config.private_dir / 'users.json'
users_before = users_json.open().read()
user = UserManager.user_manager.create() user = UserManager.user_manager.create()
user.save() user.save()
assert open("%s/users.json" % config.data_dir).read() == users_before assert users_json.open().read() == users_before

View file

@ -0,0 +1,39 @@
##
## Copyright (c) 2022 caryoscelus
##
## zeronet-conservancy is free software: you can redistribute it and/or modify it under the
## terms of the GNU General Public License as published by the Free Software
## Foundation, either version 3 of the License, or (at your option) any later version.
##
## zeronet-conservancy is distributed in the hope that it will be useful, but
## WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
## details.
##
## You should have received a copy of the GNU General Public License along with
## zeronet-conservancy. If not, see <https://www.gnu.org/licenses/>.
##
import re
from Plugin import PluginManager
# based on the code from Multiuser plugin
@PluginManager.registerTo("UiRequest")
class NoNewSites(object):
def __init__(self, *args, **kwargs):
return super(NoNewSites, self).__init__(*args, **kwargs)
def actionWrapper(self, path, extra_headers=None):
match = re.match("/(media/)?(?P<address>[A-Za-z0-9\._-]+)(?P<inner_path>/.*|$)", path)
if not match:
self.sendHeader(500)
return self.formatError("Plugin error", "No match for address found")
addr = match.group("address")
if not self.server.site_manager.get(addr):
self.sendHeader(404)
return self.formatError("Not Found", "Adding new sites disabled", details=False)
return super(NoNewSites, self).actionWrapper(path, extra_headers)
# def parsePath(self, path):
# return super(NoNewSites, self).parsePath(path)

View file

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

View file

@ -0,0 +1,5 @@
{
"name": "NoNewSites",
"description": "disables adding new sites (e.g. for proxies)",
"default": "disabled"
}

View file

@ -159,7 +159,7 @@ class UiRequestPlugin(object):
class ConfigPlugin(object): class ConfigPlugin(object):
def createArguments(self): def createArguments(self):
group = self.parser.add_argument_group("UiPassword plugin") group = self.parser.add_argument_group("UiPassword plugin")
group.add_argument('--ui_password', help='Password to access UiServer', default=None, metavar="password") group.add_argument('--ui-password', help='Password to access UiServer', default=None, metavar="password")
return super(ConfigPlugin, self).createArguments() return super(ConfigPlugin, self).createArguments()

View file

@ -1,180 +0,0 @@
import logging, json, os, re, sys, time, socket
from Plugin import PluginManager
from Config import config
from Debug import Debug
from http.client import HTTPSConnection, HTTPConnection, HTTPException
from base64 import b64encode
allow_reload = False # No reload supported
@PluginManager.registerTo("SiteManager")
class SiteManagerPlugin(object):
def load(self, *args, **kwargs):
super(SiteManagerPlugin, self).load(*args, **kwargs)
self.log = logging.getLogger("ZeronetLocal Plugin")
self.error_message = None
if not config.namecoin_host or not config.namecoin_rpcport or not config.namecoin_rpcuser or not config.namecoin_rpcpassword:
self.error_message = "Missing parameters"
self.log.error("Missing parameters to connect to namecoin node. Please check all the arguments needed with '--help'. Zeronet will continue working without it.")
return
url = "%(host)s:%(port)s" % {"host": config.namecoin_host, "port": config.namecoin_rpcport}
self.c = HTTPConnection(url, timeout=3)
user_pass = "%(user)s:%(password)s" % {"user": config.namecoin_rpcuser, "password": config.namecoin_rpcpassword}
userAndPass = b64encode(bytes(user_pass, "utf-8")).decode("ascii")
self.headers = {"Authorization" : "Basic %s" % userAndPass, "Content-Type": " application/json " }
payload = json.dumps({
"jsonrpc": "2.0",
"id": "zeronet",
"method": "ping",
"params": []
})
try:
self.c.request("POST", "/", payload, headers=self.headers)
response = self.c.getresponse()
data = response.read()
self.c.close()
if response.status == 200:
result = json.loads(data.decode())["result"]
else:
raise Exception(response.reason)
except Exception as err:
self.log.error("The Namecoin node is unreachable. Please check the configuration value are correct. Zeronet will continue working without it.")
self.error_message = err
self.cache = dict()
# 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)
# Return: Site object or None if not found
def get(self, address):
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
# 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
return super(SiteManagerPlugin, self).need(address, *args, **kwargs)
# Resolve domain
# Return: The address or None
def resolveDomain(self, domain):
domain = domain.lower()
#remove .bit on end
if domain[-4:] == ".bit":
domain = domain[0:-4]
domain_array = domain.split(".")
if self.error_message:
self.log.error("Not able to connect to Namecoin node : {!s}".format(self.error_message))
return None
if len(domain_array) > 2:
self.log.error("Too many subdomains! Can only handle one level (eg. staging.mixtape.bit)")
return None
subdomain = ""
if len(domain_array) == 1:
domain = domain_array[0]
else:
subdomain = domain_array[0]
domain = domain_array[1]
if domain in self.cache:
delta = time.time() - self.cache[domain]["time"]
if delta < 3600:
# Must have been less than 1hour
return self.cache[domain]["addresses_resolved"][subdomain]
payload = json.dumps({
"jsonrpc": "2.0",
"id": "zeronet",
"method": "name_show",
"params": ["d/"+domain]
})
try:
self.c.request("POST", "/", payload, headers=self.headers)
response = self.c.getresponse()
data = response.read()
self.c.close()
domain_object = json.loads(data.decode())["result"]
except Exception as err:
#domain doesn't exist
return None
if "zeronet" in domain_object["value"]:
zeronet_domains = json.loads(domain_object["value"])["zeronet"]
if isinstance(zeronet_domains, str):
# {
# "zeronet":"19rXKeKptSdQ9qt7omwN82smehzTuuq6S9"
# } is valid
zeronet_domains = {"": zeronet_domains}
self.cache[domain] = {"addresses_resolved": zeronet_domains, "time": time.time()}
elif "map" in domain_object["value"]:
# Namecoin standard use {"map": { "blog": {"zeronet": "1D..."} }}
data_map = json.loads(domain_object["value"])["map"]
zeronet_domains = dict()
for subdomain in data_map:
if "zeronet" in data_map[subdomain]:
zeronet_domains[subdomain] = data_map[subdomain]["zeronet"]
if "zeronet" in data_map and isinstance(data_map["zeronet"], str):
# {"map":{
# "zeronet":"19rXKeKptSdQ9qt7omwN82smehzTuuq6S9",
# }}
zeronet_domains[""] = data_map["zeronet"]
self.cache[domain] = {"addresses_resolved": zeronet_domains, "time": time.time()}
else:
# No Zeronet address registered
return None
return self.cache[domain]["addresses_resolved"][subdomain]
@PluginManager.registerTo("ConfigPlugin")
class ConfigPlugin(object):
def createArguments(self):
group = self.parser.add_argument_group("Zeroname Local plugin")
group.add_argument('--namecoin_host', help="Host to namecoin node (eg. 127.0.0.1)")
group.add_argument('--namecoin_rpcport', help="Port to connect (eg. 8336)")
group.add_argument('--namecoin_rpcuser', help="RPC user to connect to the namecoin node (eg. nofish)")
group.add_argument('--namecoin_rpcpassword', help="RPC password to connect to namecoin node")
return super(ConfigPlugin, self).createArguments()

View file

@ -1,39 +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):
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) # Get the wrapper frame output
# Is mediarequest allowed from that referer
def isMediaRequestAllowed(self, site_address, referer):
referer_path = re.sub("http[s]{0,1}://.*?/", "/", referer).replace("/media", "") # Remove site address
referer_path = re.sub(r"\?.*", "", referer_path) # Remove http params
if self.isProxyRequest(): # Match to site domain
referer = re.sub("^http://zero[/]+", "http://", referer) # Allow /zero access
referer_site_address = re.match("http[s]{0,1}://(.*?)(/|$)", referer).group(1)
else: # Match to request path
referer_site_address = re.match(r"/(?P<address>[A-Za-z0-9\.-]+)(?P<inner_path>/.*|$)", referer_path).group("address")
if referer_site_address == site_address: # Referer site address as simple address
return True
elif self.site_manager.resolveDomain(referer_site_address) == site_address: # Referer site address as dns
return True
else: # Invalid referer
return False

View file

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

View file

@ -1,9 +1,9 @@
gevent==1.4.0; python_version <= "3.6" setuptools>=70.0.0 # not directly required, pinned by Snyk to avoid a vulnerability
greenlet==0.4.16; python_version <= "3.6" gevent>=23.9.0
gevent>=20.9.0; python_version >= "3.7" msgpack>=0.6.0
msgpack>=0.4.4
base58 base58
merkletools # for some reason nobody released fresh merkletools that don't require on outdated pysha3
git+https://github.com/Tierion/pymerkletools.git@f10d71e2cd529a833728e836dc301f9af502d0b0
rsa rsa
PySocks>=1.6.8 PySocks>=1.6.8
pyasn1 pyasn1
@ -11,3 +11,9 @@ websocket_client
gevent-ws gevent-ws
coincurve coincurve
maxminddb maxminddb
rich
defusedxml>=0.7
pyaes
requests
ipython>=8
GitPython

521
src/Actions.py Normal file
View file

@ -0,0 +1,521 @@
import logging
import sys
import gevent
from Config import config
from Plugin import PluginManager
@PluginManager.acceptPlugins
class Actions:
def call(self, function_name, kwargs):
logging.info(f'zeronet-conservancy {config.version_full} on Python {sys.version} Gevent {gevent.__version__}')
func = getattr(self, function_name, None)
back = func(**kwargs)
if back:
print(back)
def ipythonThread(self):
import IPython
IPython.embed()
self.gevent_quit.set()
# Default action: Start serving UiServer and FileServer
def main(self):
import main
from File import FileServer
from Ui import UiServer
logging.info("Creating FileServer....")
main.file_server = FileServer()
logging.info("Creating UiServer....")
main.ui_server = UiServer()
main.file_server.ui_server = main.ui_server
# for startup_error in startup_errors:
# logging.error("Startup error: %s" % startup_error)
logging.info("Removing old SSL certs...")
from Crypt import CryptConnection
CryptConnection.manager.removeCerts()
logging.info("Starting servers....")
import threading
self.gevent_quit = threading.Event()
launched_greenlets = [gevent.spawn(main.ui_server.start), gevent.spawn(main.file_server.start), gevent.spawn(main.ui_server.startSiteServer)]
# if --repl, start ipython thread
# FIXME: Unfortunately this leads to exceptions on exit so use with care
if config.repl:
threading.Thread(target=self.ipythonThread).start()
stopped = 0
# Process all greenlets in main thread
while not self.gevent_quit.is_set() and stopped < len(launched_greenlets):
stopped += len(gevent.joinall(launched_greenlets, timeout=1))
# Exited due to repl, so must kill greenlets
if stopped < len(launched_greenlets):
gevent.killall(launched_greenlets, exception=KeyboardInterrupt)
logging.info("All server stopped")
# Site commands
def siteCreate(self, use_master_seed=True):
logging.info("Generating new privatekey (use_master_seed: %s)..." % config.use_master_seed)
from Crypt import CryptBitcoin
if use_master_seed:
from User import UserManager
user = UserManager.user_manager.get()
if not user:
user = UserManager.user_manager.create()
address, address_index, site_data = user.getNewSiteData()
privatekey = site_data["privatekey"]
logging.info("Generated using master seed from users.json, site index: %s" % address_index)
else:
privatekey = CryptBitcoin.newPrivatekey()
address = CryptBitcoin.privatekeyToAddress(privatekey)
logging.info("----------------------------------------------------------------------")
logging.info("Site private key: %s" % privatekey)
logging.info(" !!! ^ Save it now, required to modify the site ^ !!!")
logging.info("Site address: %s" % address)
logging.info("----------------------------------------------------------------------")
while True and not config.batch and not use_master_seed:
if input("? Have you secured your private key? (yes, no) > ").lower() == "yes":
break
else:
logging.info("Please, secure it now, you going to need it to modify your site!")
logging.info("Creating directory structure...")
from Site.Site import Site
from Site import SiteManager
SiteManager.site_manager.load()
(config.data_dir / address).mkdir()
(config.data_dir / address / 'index.html').open('w').write(f"Hello {address}!")
logging.info("Creating content.json...")
site = Site(address)
extend = {"postmessage_nonce_security": True}
if use_master_seed:
extend["address_index"] = address_index
site.content_manager.sign(privatekey=privatekey, extend=extend)
site.settings["own"] = True
site.saveSettings()
logging.info("Site created!")
def siteSign(self, address, privatekey=None, inner_path="content.json", publish=False, remove_missing_optional=False):
from Site.Site import Site
from Site import SiteManager
from Debug import Debug
SiteManager.site_manager.load()
logging.info("Signing site: %s..." % address)
site = Site(address, allow_create=False)
if not privatekey: # If no privatekey defined
from User import UserManager
user = UserManager.user_manager.get()
if user:
site_data = user.getSiteData(address)
privatekey = site_data.get("privatekey")
else:
privatekey = None
if not privatekey:
# Not found in users.json, ask from console
import getpass
privatekey = getpass.getpass("Private key (input hidden):")
# inner_path can be either relative to site directory or absolute/relative path
if os.path.isabs(inner_path):
full_path = os.path.abspath(inner_path)
else:
full_path = os.path.abspath(config.working_dir + '/' + inner_path)
print(full_path)
if os.path.isfile(full_path):
if address in full_path:
# assuming site address is unique, keep only path after it
inner_path = full_path.split(address+'/')[1]
else:
# oops, file that we found seems to be rogue, so reverting to old behaviour
logging.warning(f'using {inner_path} relative to site directory')
try:
succ = site.content_manager.sign(
inner_path=inner_path, privatekey=privatekey,
update_changed_files=True, remove_missing_optional=remove_missing_optional
)
except Exception as err:
logging.error("Sign error: %s" % Debug.formatException(err))
succ = False
if succ and publish:
self.sitePublish(address, inner_path=inner_path)
def siteVerify(self, address):
import time
from Site.Site import Site
from Site import SiteManager
SiteManager.site_manager.load()
s = time.time()
logging.info("Verifing site: %s..." % address)
site = Site(address)
bad_files = []
for content_inner_path in site.content_manager.contents:
s = time.time()
logging.info("Verifing %s signature..." % content_inner_path)
error = None
try:
file_correct = site.content_manager.verifyFile(
content_inner_path, site.storage.open(content_inner_path, "rb"), ignore_same=False
)
except Exception as err:
file_correct = False
error = err
if file_correct is True:
logging.info("[OK] %s (Done in %.3fs)" % (content_inner_path, time.time() - s))
else:
logging.error("[ERROR] %s: invalid file: %s!" % (content_inner_path, error))
input("Continue?")
bad_files += content_inner_path
logging.info("Verifying site files...")
bad_files += site.storage.verifyFiles()["bad_files"]
if not bad_files:
logging.info("[OK] All file sha512sum matches! (%.3fs)" % (time.time() - s))
else:
logging.error("[ERROR] Error during verifying site files!")
def dbRebuild(self, address):
from Site.Site import Site
from Site import SiteManager
SiteManager.site_manager.load()
logging.info("Rebuilding site sql cache: %s..." % address)
site = SiteManager.site_manager.get(address)
s = time.time()
try:
site.storage.rebuildDb()
logging.info("Done in %.3fs" % (time.time() - s))
except Exception as err:
logging.error(err)
def dbQuery(self, address, query):
from Site.Site import Site
from Site import SiteManager
SiteManager.site_manager.load()
import json
site = Site(address)
result = []
for row in site.storage.query(query):
result.append(dict(row))
print(json.dumps(result, indent=4))
def siteAnnounce(self, address):
from Site.Site import Site
from Site import SiteManager
SiteManager.site_manager.load()
logging.info("Opening a simple connection server")
from File import FileServer
main.file_server = FileServer("127.0.0.1", 1234)
main.file_server.start()
logging.info("Announcing site %s to tracker..." % address)
site = Site(address)
s = time.time()
site.announce()
print("Response time: %.3fs" % (time.time() - s))
print(site.peers)
def siteDownload(self, address):
from Site.Site import Site
from Site import SiteManager
SiteManager.site_manager.load()
logging.info("Opening a simple connection server")
from File import FileServer
main.file_server = FileServer("127.0.0.1", 1234)
file_server_thread = gevent.spawn(main.file_server.start, check_sites=False)
site = Site(address)
on_completed = gevent.event.AsyncResult()
def onComplete(evt):
evt.set(True)
site.onComplete.once(lambda: onComplete(on_completed))
print("Announcing...")
site.announce()
s = time.time()
print("Downloading...")
site.downloadContent("content.json", check_modifications=True)
print("Downloaded in %.3fs" % (time.time()-s))
def siteNeedFile(self, address, inner_path):
from Site.Site import Site
from Site import SiteManager
SiteManager.site_manager.load()
def checker():
while 1:
s = time.time()
time.sleep(1)
print("Switch time:", time.time() - s)
gevent.spawn(checker)
logging.info("Opening a simple connection server")
from File import FileServer
main.file_server = FileServer("127.0.0.1", 1234)
file_server_thread = gevent.spawn(main.file_server.start, check_sites=False)
site = Site(address)
site.announce()
print(site.needFile(inner_path, update=True))
def siteCmd(self, address, cmd, parameters):
import json
from Site import SiteManager
site = SiteManager.site_manager.get(address)
if not site:
logging.error("Site not found: %s" % address)
return None
ws = self.getWebsocket(site)
ws.send(json.dumps({"cmd": cmd, "params": parameters, "id": 1}))
res_raw = ws.recv()
try:
res = json.loads(res_raw)
except Exception as err:
return {"error": "Invalid result: %s" % err, "res_raw": res_raw}
if "result" in res:
return res["result"]
else:
return res
def importBundle(self, bundle):
import main
main.importBundle(bundle)
def getWebsocket(self, site):
import websocket
ws_address = "ws://%s:%s/Websocket?wrapper_key=%s" % (config.ui_ip, config.ui_port, site.settings["wrapper_key"])
logging.info("Connecting to %s" % ws_address)
ws = websocket.create_connection(ws_address)
return ws
def sitePublish(self, address, peer_ip=None, peer_port=15441, inner_path="content.json", recursive=False):
from Site import SiteManager
logging.info("Loading site...")
site = SiteManager.site_manager.get(address)
site.settings["serving"] = True # Serving the site even if its disabled
if not recursive:
inner_paths = [inner_path]
else:
inner_paths = list(site.content_manager.contents.keys())
try:
ws = self.getWebsocket(site)
except Exception as err:
self.sitePublishFallback(site, peer_ip, peer_port, inner_paths, err)
else:
logging.info("Sending siteReload")
self.siteCmd(address, "siteReload", inner_path)
for inner_path in inner_paths:
logging.info(f"Sending sitePublish for {inner_path}")
self.siteCmd(address, "sitePublish", {"inner_path": inner_path, "sign": False})
logging.info("Done.")
ws.close()
def sitePublishFallback(self, site, peer_ip, peer_port, inner_paths, err):
if err is not None:
logging.info(f"Can't connect to local websocket client: {err}")
logging.info("Publish using fallback mechanism. "
"Note that there might be not enough time for peer discovery, "
"but you can specify target peer on command line.")
logging.info("Creating FileServer....")
file_server_thread = gevent.spawn(main.file_server.start, check_sites=False) # Dont check every site integrity
time.sleep(0.001)
# Started fileserver
main.file_server.portCheck()
if peer_ip: # Announce ip specificed
site.addPeer(peer_ip, peer_port)
else: # Just ask the tracker
logging.info("Gathering peers from tracker")
site.announce() # Gather peers
for inner_path in inner_paths:
published = site.publish(5, inner_path) # Push to peers
if published > 0:
time.sleep(3)
logging.info("Serving files (max 60s)...")
gevent.joinall([file_server_thread], timeout=60)
logging.info("Done.")
else:
logging.info("No peers found, sitePublish command only works if you already have visitors serving your site")
# Crypto commands
def cryptPrivatekeyToAddress(self, privatekey=None):
from Crypt import CryptBitcoin
if not privatekey: # If no privatekey in args then ask it now
import getpass
privatekey = getpass.getpass("Private key (input hidden):")
print(CryptBitcoin.privatekeyToAddress(privatekey))
def cryptSign(self, message, privatekey):
from Crypt import CryptBitcoin
print(CryptBitcoin.sign(message, privatekey))
def cryptVerify(self, message, sign, address):
from Crypt import CryptBitcoin
print(CryptBitcoin.verify(message, address, sign))
def cryptGetPrivatekey(self, master_seed, site_address_index=None):
from Crypt import CryptBitcoin
if len(master_seed) != 64:
logging.error("Error: Invalid master seed length: %s (required: 64)" % len(master_seed))
return False
privatekey = CryptBitcoin.hdPrivatekey(master_seed, site_address_index)
print("Requested private key: %s" % privatekey)
# Peer
def peerPing(self, peer_ip, peer_port=None):
if not peer_port:
peer_port = 15441
logging.info("Opening a simple connection server")
from Connection import ConnectionServer
main.file_server = ConnectionServer("127.0.0.1", 1234)
main.file_server.start(check_connections=False)
from Crypt import CryptConnection
CryptConnection.manager.loadCerts()
from Peer import Peer
logging.info("Pinging 5 times peer: %s:%s..." % (peer_ip, int(peer_port)))
s = time.time()
peer = Peer(peer_ip, peer_port)
peer.connect()
if not peer.connection:
print("Error: Can't connect to peer (connection error: %s)" % peer.connection_error)
return False
if "shared_ciphers" in dir(peer.connection.sock):
print("Shared ciphers:", peer.connection.sock.shared_ciphers())
if "cipher" in dir(peer.connection.sock):
print("Cipher:", peer.connection.sock.cipher()[0])
if "version" in dir(peer.connection.sock):
print("TLS version:", peer.connection.sock.version())
print("Connection time: %.3fs (connection error: %s)" % (time.time() - s, peer.connection_error))
for i in range(5):
ping_delay = peer.ping()
print("Response time: %.3fs" % ping_delay)
time.sleep(1)
peer.remove()
print("Reconnect test...")
peer = Peer(peer_ip, peer_port)
for i in range(5):
ping_delay = peer.ping()
print("Response time: %.3fs" % ping_delay)
time.sleep(1)
def peerGetFile(self, peer_ip, peer_port, site, filename, benchmark=False):
logging.info("Opening a simple connection server")
from Connection import ConnectionServer
main.file_server = ConnectionServer("127.0.0.1", 1234)
main.file_server.start(check_connections=False)
from Crypt import CryptConnection
CryptConnection.manager.loadCerts()
from Peer import Peer
logging.info("Getting %s/%s from peer: %s:%s..." % (site, filename, peer_ip, peer_port))
peer = Peer(peer_ip, peer_port)
s = time.time()
if benchmark:
for i in range(10):
peer.getFile(site, filename),
print("Response time: %.3fs" % (time.time() - s))
input("Check memory")
else:
print(peer.getFile(site, filename).read())
def peerCmd(self, peer_ip, peer_port, cmd, parameters):
logging.info("Opening a simple connection server")
from Connection import ConnectionServer
main.file_server = ConnectionServer()
main.file_server.start(check_connections=False)
from Crypt import CryptConnection
CryptConnection.manager.loadCerts()
from Peer import Peer
peer = Peer(peer_ip, peer_port)
import json
if parameters:
parameters = json.loads(parameters.replace("'", '"'))
else:
parameters = {}
try:
res = peer.request(cmd, parameters)
print(json.dumps(res, indent=2, ensure_ascii=False))
except Exception as err:
print("Unknown response (%s): %s" % (err, res))
def getConfig(self):
import json
print(json.dumps(config.getServerInfo(), indent=2, ensure_ascii=False))
def test(self, test_name, *args, **kwargs):
import types
def funcToName(func_name):
test_name = func_name.replace("test", "")
return test_name[0].lower() + test_name[1:]
test_names = [funcToName(name) for name in dir(self) if name.startswith("test") and name != "test"]
if not test_name:
# No test specificed, list tests
print("\nNo test specified, possible tests:")
for test_name in test_names:
func_name = "test" + test_name[0].upper() + test_name[1:]
func = getattr(self, func_name)
if func.__doc__:
print("- %s: %s" % (test_name, func.__doc__.strip()))
else:
print("- %s" % test_name)
return None
# Run tests
func_name = "test" + test_name[0].upper() + test_name[1:]
if hasattr(self, func_name):
func = getattr(self, func_name)
print("- Running test: %s" % test_name, end="")
s = time.time()
ret = func(*args, **kwargs)
if type(ret) is types.GeneratorType:
for progress in ret:
print(progress, end="")
sys.stdout.flush()
print("\n* Test %s done in %.3fs" % (test_name, time.time() - s))
else:
print("Unknown test: %r (choose from: %s)" % (
test_name, test_names
))

View file

@ -1,6 +1,7 @@
import argparse import argparse
import sys import sys
import os import os
import platform
import locale import locale
import re import re
import configparser import configparser
@ -8,13 +9,39 @@ import logging
import logging.handlers import logging.handlers
import stat import stat
import time import time
from pathlib import Path
VERSION = "0.7.10+"
class Config(object): class StartupError(RuntimeError):
pass
class Config:
"""Class responsible for storing and loading config.
Used as singleton `config`
"""
def __init__(self, argv): def __init__(self, argv):
self.version = "0.7.2" try:
self.rev = 4555 from . import Build
except ImportError:
from .util import Git
self.build_type = 'source'
self.branch = Git.branch() or 'unknown'
self.commit = Git.commit() or 'unknown'
self.version = VERSION
self.platform = 'source'
else:
self.build_type = Build.build_type
self.branch = Build.branch
self.commit = Build.commit
self.version = Build.version or VERSION
self.platform = Build.platform
self.version_full = f'{self.version} ({self.build_type} from {self.branch}-{self.commit})'
self.user_agent = "conservancy"
# for compatibility
self.user_agent_rev = 8192
self.argv = argv self.argv = argv
self.action = None self.action = None
self.test_parser = None self.test_parser = None
@ -28,15 +55,18 @@ class Config(object):
self.keys_restart_need = set([ self.keys_restart_need = set([
"tor", "fileserver_port", "fileserver_ip_type", "threads_fs_read", "threads_fs_write", "threads_crypt", "threads_db" "tor", "fileserver_port", "fileserver_ip_type", "threads_fs_read", "threads_fs_write", "threads_crypt", "threads_db"
]) ])
self.start_dir = self.getStartDir()
self.config_file = self.start_dir + "/zeronet.conf" self.config_file = None
self.data_dir = self.start_dir + "/data" self.config_dir = None
self.log_dir = self.start_dir + "/log" self.data_dir = None
self.private_dir = None
self.log_dir = None
self.configurePaths(argv)
self.openssl_lib_file = None self.openssl_lib_file = None
self.openssl_bin_file = None self.openssl_bin_file = None
self.trackers_file = False self.trackers_file = None
self.createParser() self.createParser()
self.createArguments() self.createArguments()
@ -53,11 +83,12 @@ class Config(object):
def strToBool(self, v): def strToBool(self, v):
return v.lower() in ("yes", "true", "t", "1") return v.lower() in ("yes", "true", "t", "1")
def getStartDir(self): def getStartDirOld(self):
"""Get directory that would have been used by older versions (pre v0.7.11)"""
this_file = os.path.abspath(__file__).replace("\\", "/").rstrip("cd") this_file = os.path.abspath(__file__).replace("\\", "/").rstrip("cd")
if "--start_dir" in self.argv: if "--start-dir" in self.argv:
start_dir = self.argv[self.argv.index("--start_dir") + 1] start_dir = self.argv[self.argv.index("--start-dir") + 1]
elif this_file.endswith("/Contents/Resources/core/src/Config.py"): elif this_file.endswith("/Contents/Resources/core/src/Config.py"):
# Running as ZeroNet.app # Running as ZeroNet.app
if this_file.startswith("/Application") or this_file.startswith("/private") or this_file.startswith(os.path.expanduser("~/Library")): if this_file.startswith("/Application") or this_file.startswith("/private") or this_file.startswith(os.path.expanduser("~/Library")):
@ -69,33 +100,134 @@ class Config(object):
elif this_file.endswith("/core/src/Config.py"): elif this_file.endswith("/core/src/Config.py"):
# Running as exe or source is at Application Support directory, put var files to outside of core dir # Running as exe or source is at Application Support directory, put var files to outside of core dir
start_dir = this_file.replace("/core/src/Config.py", "") start_dir = this_file.replace("/core/src/Config.py", "")
elif this_file.endswith("usr/share/zeronet/src/Config.py"): elif not os.access(this_file.replace('/src/Config.py', ''), os.R_OK | os.W_OK):
# Running from non-writeable location, e.g., AppImage # Running from non-writeable location, e.g., AppImage
start_dir = os.path.expanduser("~/ZeroNet") start_dir = os.path.expanduser("~/ZeroNet")
else: else:
start_dir = "." start_dir = "."
return start_dir return start_dir
def migrateOld(self, source):
print(f'[bold red]WARNING: found data {source}[/bold red]')
print( ' It used to be default behaviour to store data there,')
print( ' but now we default to place data and config in user home directory.')
print( '')
def configurePaths(self, argv):
if '--config-file' in argv:
self.config_file = argv[argv.index('--config-file') + 1]
old_dir = Path(self.getStartDirOld())
new_dir = Path(self.getStartDir())
no_migrate = '--no-migrate' in argv
silent_migrate = '--portable' in argv or '--migrate' in argv
try:
self.start_dir = self.maybeMigrate(old_dir, new_dir, no_migrate, silent_migrate)
except Exception as ex:
raise ex
self.updatePaths()
def updatePaths(self):
if self.config_file is None:
self.config_file = self.start_dir / 'znc.conf'
if self.config_dir is None:
self.config_dir = self.start_dir
if self.private_dir is None:
self.private_dir = self.start_dir / 'private'
if self.data_dir is None:
self.data_dir = self.start_dir / 'data'
if self.log_dir is None:
self.log_dir = self.start_dir / 'log'
def createPaths(self):
self.start_dir.mkdir(parents=True, exist_ok=True)
self.private_dir.mkdir(parents=True, exist_ok=True)
self.data_dir.mkdir(parents=True, exist_ok=True)
self.log_dir.mkdir(parents=True, exist_ok=True)
def checkDir(self, root):
return (root / 'znc.conf').is_file()
def doMigrate(self, old_dir, new_dir):
raise RuntimeError('Migration not implemented yet')
def askMigrate(self, old_dir, new_dir, silent):
if not sys.stdin.isatty():
raise StartupError('Migration refused: non-interactive shell')
while True:
r = input(f'You have old data in `{old_dir}`. Migrate to new format to `{new_dir}`? [Y/n]')
if r.lower().startswith('n'):
raise StartupError('Migration refused')
if r.lower().startswith('y'):
return self.doMigrate(old_dir, new_dir)
def createNewConfig(self, new_dir):
new_dir.mkdir(parents=True, exist_ok=True)
with (new_dir / 'znc.conf').open('w') as f:
f.write('# zeronet-conervancy config file')
def maybeMigrate(self, old_dir, new_dir, no_migrate, silent_migrate):
if old_dir.exists() and new_dir.exists():
if old_dir == new_dir:
if self.checkDir(new_dir):
return new_dir
elif no_migrate:
return StartError('Migration refused, but new directory should be migrated')
else:
return askMigrate(old_dir, new_dir, silent_migrate)
else:
if self.checkDir(new_dir):
if not no_migrate:
print("There's an old starting directory")
return new_dir
else:
raise StartupError('Bad startup directory')
elif old_dir.exists():
if no_migrate:
self.createNewConfig(new_dir)
return new_dir
else:
return self.askMigrate(old_dir, new_dir, silent_migrate)
elif new_dir.exists():
if self.checkDir(new_dir):
return new_dir
else:
return StartupError('Bad startup directory')
else:
self.createNewConfig(new_dir)
return new_dir
def getStartDir(self):
"""Return directory with config & data"""
if "--start-dir" in self.argv:
return self.argv[self.argv.index("--start-dir") + 1]
here = os.path.dirname(os.path.abspath(__file__).replace("\\", "/")).rstrip('/src')
if '--portable' in self.argv or self.build_type == 'portable':
return here
MACOSX_DIR = '~/Library/Application Support/zeronet-conservancy'
WINDOWS_DIR = '~/AppData/zeronet-conservancy'
LIBREDESKTOP_DIR = '~/.local/share/zeronet-conservancy'
if self.platform == 'source':
if platform.system() == 'Darwin':
path = MACOSX_DIR
elif platform.system() == 'Windows':
path = WINDOWS_DIR
else:
path = LIBREDESKTOP_DIR
elif self.platform == 'macosx':
path = MACOSX_DIR
elif self.platform == 'windows':
path = WINDOWS_DIR
elif self.platform == 'libredesktop':
path = LIBREDESKTOP_DIR
else:
raise RuntimeError(f'UNKNOWN PLATFORM: {self.platform}. Something must have went terribly wrong!')
return os.path.expanduser(path)
# Create command line arguments # Create command line arguments
def createArguments(self): def createArguments(self):
trackers = [
"zero://boot3rdez4rzn36x.onion:15441",
"zero://zero.booth.moe#f36ca555bee6ba216b14d10f38c16f7769ff064e0e37d887603548cc2e64191d:443", # US/NY
"udp://tracker.coppersurfer.tk:6969", # DE
"udp://104.238.198.186:8000", # US/LA
"udp://retracker.akado-ural.ru:80", # RU
"http://h4.trakx.nibba.trade:80/announce", # US/VA
"http://open.acgnxtracker.com:80/announce", # DE
"http://tracker.bt4g.com:2095/announce", # Cloudflare
"zero://2602:ffc5::c5b2:5360:26312" # US/ATL
]
# Platform specific
if sys.platform.startswith("win"):
coffeescript = "type %s | tools\\coffee\\coffee.cmd"
else:
coffeescript = None
try: try:
language, enc = locale.getdefaultlocale() language, enc = locale.getdefaultlocale()
language = language.lower().replace("_", "-") language = language.lower().replace("_", "-")
@ -111,9 +243,9 @@ class Config(object):
else: else:
fix_float_decimals = False fix_float_decimals = False
config_file = self.start_dir + "/zeronet.conf" config_file = self.config_file
data_dir = self.start_dir + "/data" data_dir = self.data_dir
log_dir = self.start_dir + "/log" log_dir = self.log_dir
ip_local = ["127.0.0.1", "::1"] ip_local = ["127.0.0.1", "::1"]
@ -123,7 +255,7 @@ class Config(object):
# SiteCreate # SiteCreate
action = self.subparsers.add_parser("siteCreate", help='Create a new site') action = self.subparsers.add_parser("siteCreate", help='Create a new site')
action.register('type', 'bool', self.strToBool) action.register('type', 'bool', self.strToBool)
action.add_argument('--use_master_seed', help="Allow created site's private key to be recovered using the master seed in users.json (default: True)", type="bool", choices=[True, False], default=True) action.add_argument('--use-master_seed', help="Allow created site's private key to be recovered using the master seed in users.json (default: True)", type="bool", choices=[True, False], default=True)
# SiteNeedFile # SiteNeedFile
action = self.subparsers.add_parser("siteNeedFile", help='Get a file from site') action = self.subparsers.add_parser("siteNeedFile", help='Get a file from site')
@ -138,9 +270,9 @@ class Config(object):
action = self.subparsers.add_parser("siteSign", help='Update and sign content.json: address [privatekey]') action = self.subparsers.add_parser("siteSign", help='Update and sign content.json: address [privatekey]')
action.add_argument('address', help='Site to sign') action.add_argument('address', help='Site to sign')
action.add_argument('privatekey', help='Private key (default: ask on execute)', nargs='?') action.add_argument('privatekey', help='Private key (default: ask on execute)', nargs='?')
action.add_argument('--inner_path', help='File you want to sign (default: content.json)', action.add_argument('--inner-path', help='File you want to sign (default: content.json)',
default="content.json", metavar="inner_path") default="content.json", metavar="inner_path")
action.add_argument('--remove_missing_optional', help='Remove optional files that is not present in the directory', action='store_true') action.add_argument('--remove-missing_optional', help='Remove optional files that is not present in the directory', action='store_true')
action.add_argument('--publish', help='Publish site after the signing', action='store_true') action.add_argument('--publish', help='Publish site after the signing', action='store_true')
# SitePublish # SitePublish
@ -150,8 +282,10 @@ class Config(object):
default=None, nargs='?') default=None, nargs='?')
action.add_argument('peer_port', help='Peer port to publish (default: random peer port from tracker)', action.add_argument('peer_port', help='Peer port to publish (default: random peer port from tracker)',
default=15441, nargs='?') default=15441, nargs='?')
action.add_argument('--inner_path', help='Content.json you want to publish (default: content.json)', action.add_argument('--inner-path', help='Content.json you want to publish (default: content.json)',
default="content.json", metavar="inner_path") default="content.json", metavar="inner_path")
action.add_argument('--recursive', help="Whether to publish all of site's content.json. "
"Overrides --inner-path. (default: false)", action='store_true', dest='recursive')
# SiteVerify # SiteVerify
action = self.subparsers.add_parser("siteVerify", help='Verify site files using sha512: address') action = self.subparsers.add_parser("siteVerify", help='Verify site files using sha512: address')
@ -163,6 +297,10 @@ class Config(object):
action.add_argument('cmd', help='API command name') action.add_argument('cmd', help='API command name')
action.add_argument('parameters', help='Parameters of the command', nargs='?') action.add_argument('parameters', help='Parameters of the command', nargs='?')
# Import bundled sites
action = self.subparsers.add_parser("importBundle", help='Import sites from a .zip bundle')
action.add_argument('bundle', help='Path to a data bundle')
# dbRebuild # dbRebuild
action = self.subparsers.add_parser("dbRebuild", help='Rebuild site database cache') action = self.subparsers.add_parser("dbRebuild", help='Rebuild site database cache')
action.add_argument('address', help='Site to rebuild') action.add_argument('address', help='Site to rebuild')
@ -220,105 +358,113 @@ class Config(object):
self.parser.add_argument('--verbose', help='More detailed logging', action='store_true') self.parser.add_argument('--verbose', help='More detailed logging', action='store_true')
self.parser.add_argument('--debug', help='Debug mode', action='store_true') self.parser.add_argument('--debug', help='Debug mode', action='store_true')
self.parser.add_argument('--silent', help='Only log errors to terminal output', action='store_true') self.parser.add_argument('--silent', help='Only log errors to terminal output', action='store_true')
self.parser.add_argument('--debug_socket', help='Debug socket connections', action='store_true') self.parser.add_argument('--debug-socket', help='Debug socket connections', action='store_true')
self.parser.add_argument('--merge_media', help='Merge all.js and all.css', action='store_true') self.parser.add_argument('--merge-media', help='Merge all.js and all.css', action='store_true')
self.parser.add_argument('--batch', help="Batch mode (No interactive input for commands)", action='store_true') self.parser.add_argument('--batch', help="Batch mode (No interactive input for commands)", action='store_true')
self.parser.add_argument('--start_dir', help='Path of working dir for variable content (data, log, .conf)', default=self.start_dir, metavar="path") self.parser.add_argument('--portable', action=argparse.BooleanOptionalAction)
self.parser.add_argument('--config_file', help='Path of config file', default=config_file, metavar="path") self.parser.add_argument('--start-dir', help='Path of working dir for variable content (data, log, config)', default=self.start_dir, metavar="path")
self.parser.add_argument('--data_dir', help='Path of data directory', default=data_dir, metavar="path") self.parser.add_argument('--config-file', help='Path of config file', default=config_file, metavar="path")
self.parser.add_argument('--data-dir', help='Path of data directory', default=data_dir, metavar="path")
self.parser.add_argument('--no-migrate', help='Ignore data directories from old 0net versions', action=argparse.BooleanOptionalAction, default=False)
self.parser.add_argument('--console_log_level', help='Level of logging to console', default="default", choices=["default", "DEBUG", "INFO", "ERROR", "off"]) self.parser.add_argument('--console-log-level', help='Level of logging to console', default="default", choices=["default", "DEBUG", "INFO", "ERROR", "off"])
self.parser.add_argument('--log_dir', help='Path of logging directory', default=log_dir, metavar="path") self.parser.add_argument('--log-dir', help='Path of logging directory', default=log_dir, metavar="path")
self.parser.add_argument('--log_level', help='Level of logging to file', default="DEBUG", choices=["DEBUG", "INFO", "ERROR", "off"]) self.parser.add_argument('--log-level', help='Level of logging to file', default="DEBUG", choices=["DEBUG", "INFO", "ERROR", "off"])
self.parser.add_argument('--log_rotate', help='Log rotate interval', default="daily", choices=["hourly", "daily", "weekly", "off"]) self.parser.add_argument('--log-rotate', help='Log rotate interval', default="daily", choices=["hourly", "daily", "weekly", "off"])
self.parser.add_argument('--log_rotate_backup_count', help='Log rotate backup count', default=5, type=int) self.parser.add_argument('--log-rotate-backup-count', help='Log rotate backup count', default=5, type=int)
self.parser.add_argument('--language', help='Web interface language', default=language, metavar='language') self.parser.add_argument('--language', help='Web interface language', default=language, metavar='language')
self.parser.add_argument('--ui_ip', help='Web interface bind address', default="127.0.0.1", metavar='ip') self.parser.add_argument('--ui-ip-protect', help="Protect UI server from being accessed through third-party pages and on unauthorized cross-origin pages (enabled by default when serving on localhost IPs; doesn't work with non-local IPs, need testing with host names)", choices=['always', 'local', 'off'], default='local')
self.parser.add_argument('--ui_port', help='Web interface bind port', default=43110, type=int, metavar='port') self.parser.add_argument('--ui-ip', help='Web interface bind address', default="127.0.0.1", metavar='ip')
self.parser.add_argument('--ui_restrict', help='Restrict web access', default=False, metavar='ip', nargs='*') self.parser.add_argument('--ui-port', help='Web interface bind port', default=43110, type=int, metavar='port')
self.parser.add_argument('--ui_host', help='Allow access using this hosts', metavar='host', nargs='*') self.parser.add_argument('--ui-site-port', help='Port for serving site content, defaults to ui_port+1', default=None, metavar='port')
self.parser.add_argument('--ui_trans_proxy', help='Allow access using a transparent proxy', action='store_true') self.parser.add_argument('--ui-restrict', help='Restrict web access', default=False, metavar='ip', nargs='*')
self.parser.add_argument('--ui-host', help='Allow access using this hosts', metavar='host', nargs='*')
self.parser.add_argument('--ui-trans-proxy', help='Allow access using a transparent proxy', action='store_true')
self.parser.add_argument('--open_browser', help='Open homepage in web browser automatically', self.parser.add_argument('--open-browser', help='Open homepage in web browser automatically',
nargs='?', const="default_browser", metavar='browser_name') nargs='?', const="default_browser", metavar='browser_name')
self.parser.add_argument('--homepage', help='Web interface Homepage', default='1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D', self.parser.add_argument('--homepage', help='Web interface Homepage', default='191CazMVNaAcT9Y1zhkxd9ixMBPs59g2um',
metavar='address') metavar='address')
self.parser.add_argument('--updatesite', help='Source code update site', default='1uPDaT3uSyWAPdCv1WkMb5hBQjWSNNACf', # self.parser.add_argument('--updatesite', help='Source code update site', default='1uPDaT3uSyWAPdCv1WkMb5hBQjWSNNACf',
metavar='address') # metavar='address')
self.parser.add_argument('--dist_type', help='Type of installed distribution', default='source') self.parser.add_argument('--admin-pages', help='Pages with admin privileges', default=[], metavar='address', nargs='*')
self.parser.add_argument('--dist-type', help='Type of installed distribution', default='source')
self.parser.add_argument('--size_limit', help='Default site size limit in MB', default=10, type=int, metavar='limit') self.parser.add_argument('--size-limit', help='Default site size limit in MB', default=10, type=int, metavar='limit')
self.parser.add_argument('--file_size_limit', help='Maximum per file size limit in MB', default=10, type=int, metavar='limit') self.parser.add_argument('--file-size-limit', help='Maximum per file size limit in MB', default=10, type=int, metavar='limit')
self.parser.add_argument('--connected_limit', help='Max connected peer per site', default=8, type=int, metavar='connected_limit') self.parser.add_argument('--connected-limit', help='Max connected peer per site', default=8, type=int, metavar='connected_limit')
self.parser.add_argument('--global_connected_limit', help='Max connections', default=512, type=int, metavar='global_connected_limit') self.parser.add_argument('--global-connected-limit', help='Max connections', default=512, type=int, metavar='global_connected_limit')
self.parser.add_argument('--workers', help='Download workers per site', default=5, type=int, metavar='workers') self.parser.add_argument('--workers', help='Download workers per site', default=5, type=int, metavar='workers')
self.parser.add_argument('--fileserver_ip', help='FileServer bind address', default="*", metavar='ip') self.parser.add_argument('--fileserver-ip', help='FileServer bind address', default="*", metavar='ip')
self.parser.add_argument('--fileserver_port', help='FileServer bind port (0: randomize)', default=0, type=int, metavar='port') self.parser.add_argument('--fileserver-port', help='FileServer bind port (0: randomize)', default=0, type=int, metavar='port')
self.parser.add_argument('--fileserver_port_range', help='FileServer randomization range', default="10000-40000", metavar='port') self.parser.add_argument('--fileserver-port-range', help='FileServer randomization range', default="10000-40000", metavar='port')
self.parser.add_argument('--fileserver_ip_type', help='FileServer ip type', default="dual", choices=["ipv4", "ipv6", "dual"]) self.parser.add_argument('--fileserver-ip-type', help='FileServer ip type', default="dual", choices=["ipv4", "ipv6", "dual"])
self.parser.add_argument('--ip_local', help='My local ips', default=ip_local, type=int, metavar='ip', nargs='*') self.parser.add_argument('--ip-local', help='My local ips', default=ip_local, type=int, metavar='ip', nargs='*')
self.parser.add_argument('--ip_external', help='Set reported external ip (tested on start if None)', metavar='ip', nargs='*') self.parser.add_argument('--ip-external', help='Set reported external ip (tested on start if None)', metavar='ip', nargs='*')
self.parser.add_argument('--offline', help='Disable network communication', action='store_true') self.parser.add_argument('--offline', help='Disable network communication', action='store_true')
self.parser.add_argument('--disable-port-check', help='Disable checking port', action='store_true')
self.parser.add_argument('--disable_udp', help='Disable UDP connections', action='store_true') self.parser.add_argument('--disable-udp', help='Disable UDP connections', action='store_true')
self.parser.add_argument('--proxy', help='Socks proxy address', metavar='ip:port') self.parser.add_argument('--proxy', help='Socks proxy address', metavar='ip:port')
self.parser.add_argument('--bind', help='Bind outgoing sockets to this address', metavar='ip') self.parser.add_argument('--bind', help='Bind outgoing sockets to this address', metavar='ip')
self.parser.add_argument('--trackers', help='Bootstraping torrent trackers', default=trackers, metavar='protocol://address', nargs='*') self.parser.add_argument('--bootstrap-url', help='URL of file with link to bootstrap bundle', default='https://raw.githubusercontent.com/zeronet-conservancy/zeronet-conservancy/master/bootstrap.url', type=str)
self.parser.add_argument('--trackers_file', help='Load torrent trackers dynamically from a file', metavar='path', nargs='*') self.parser.add_argument('--bootstrap', help='Enable downloading bootstrap information from clearnet', action=argparse.BooleanOptionalAction, default=True)
self.parser.add_argument('--trackers_proxy', help='Force use proxy to connect to trackers (disable, tor, ip:port)', default="disable") self.parser.add_argument('--trackers', help='Bootstraping torrent trackers', default=[], metavar='protocol://address', nargs='*')
self.parser.add_argument('--use_libsecp256k1', help='Use Libsecp256k1 liblary for speedup', type='bool', choices=[True, False], default=True) self.parser.add_argument('--trackers-file', help='Load torrent trackers dynamically from a file (using Syncronite by default)', default=['{data_dir}/15CEFKBRHFfAP9rmL6hhLmHoXrrgmw4B5o/cache/1/Syncronite.html'], metavar='path', nargs='*')
self.parser.add_argument('--use_openssl', help='Use OpenSSL liblary for speedup', type='bool', choices=[True, False], default=True) self.parser.add_argument('--trackers-proxy', help='Force use proxy to connect to trackers (disable, tor, ip:port)', default="disable")
self.parser.add_argument('--openssl_lib_file', help='Path for OpenSSL library file (default: detect)', default=argparse.SUPPRESS, metavar="path") self.parser.add_argument('--use-libsecp256k1', help='Use Libsecp256k1 liblary for speedup', type='bool', choices=[True, False], default=True)
self.parser.add_argument('--openssl_bin_file', help='Path for OpenSSL binary file (default: detect)', default=argparse.SUPPRESS, metavar="path") self.parser.add_argument('--use-openssl', help='Use OpenSSL liblary for speedup', type='bool', choices=[True, False], default=True)
self.parser.add_argument('--disable_db', help='Disable database updating', action='store_true') self.parser.add_argument('--openssl-lib-file', help='Path for OpenSSL library file (default: detect)', default=argparse.SUPPRESS, metavar="path")
self.parser.add_argument('--disable_encryption', help='Disable connection encryption', action='store_true') self.parser.add_argument('--openssl-bin-file', help='Path for OpenSSL binary file (default: detect)', default=argparse.SUPPRESS, metavar="path")
self.parser.add_argument('--force_encryption', help="Enforce encryption to all peer connections", action='store_true') self.parser.add_argument('--disable-db', help='Disable database updating', action='store_true')
self.parser.add_argument('--disable_sslcompression', help='Disable SSL compression to save memory', self.parser.add_argument('--disable-encryption', help='Disable connection encryption', action='store_true')
self.parser.add_argument('--force-encryption', help="Enforce encryption to all peer connections", action='store_true')
self.parser.add_argument('--disable-sslcompression', help='Disable SSL compression to save memory',
type='bool', choices=[True, False], default=True) type='bool', choices=[True, False], default=True)
self.parser.add_argument('--keep_ssl_cert', help='Disable new SSL cert generation on startup', action='store_true') self.parser.add_argument('--keep-ssl-cert', help='Disable new SSL cert generation on startup', action='store_true')
self.parser.add_argument('--max_files_opened', help='Change maximum opened files allowed by OS to this value on startup', self.parser.add_argument('--max-files-opened', help='Change maximum opened files allowed by OS to this value on startup',
default=2048, type=int, metavar='limit') default=2048, type=int, metavar='limit')
self.parser.add_argument('--stack_size', help='Change thread stack size', default=None, type=int, metavar='thread_stack_size') self.parser.add_argument('--stack-size', help='Change thread stack size', default=None, type=int, metavar='thread_stack_size')
self.parser.add_argument('--use_tempfiles', help='Use temporary files when downloading (experimental)', self.parser.add_argument('--use-tempfiles', help='Use temporary files when downloading (experimental)',
type='bool', choices=[True, False], default=False) type='bool', choices=[True, False], default=False)
self.parser.add_argument('--stream_downloads', help='Stream download directly to files (experimental)', self.parser.add_argument('--stream-downloads', help='Stream download directly to files (experimental)',
type='bool', choices=[True, False], default=False) type='bool', choices=[True, False], default=False)
self.parser.add_argument("--msgpack_purepython", help='Use less memory, but a bit more CPU power', self.parser.add_argument('--msgpack-purepython', help='Use less memory, but a bit more CPU power',
type='bool', choices=[True, False], default=False) type='bool', choices=[True, False], default=False)
self.parser.add_argument("--fix_float_decimals", help='Fix content.json modification date float precision on verification', self.parser.add_argument('--fix-float-decimals', help='Fix content.json modification date float precision on verification',
type='bool', choices=[True, False], default=fix_float_decimals) type='bool', choices=[True, False], default=fix_float_decimals)
self.parser.add_argument("--db_mode", choices=["speed", "security"], default="speed") self.parser.add_argument('--db-mode', choices=["speed", "security"], default="speed")
self.parser.add_argument('--threads_fs_read', help='Number of threads for file read operations', default=1, type=int) self.parser.add_argument('--threads-fs-read', help='Number of threads for file read operations', default=1, type=int)
self.parser.add_argument('--threads_fs_write', help='Number of threads for file write operations', default=1, type=int) self.parser.add_argument('--threads-fs-write', help='Number of threads for file write operations', default=1, type=int)
self.parser.add_argument('--threads_crypt', help='Number of threads for cryptographic operations', default=2, type=int) self.parser.add_argument('--threads-crypt', help='Number of threads for cryptographic operations', default=2, type=int)
self.parser.add_argument('--threads_db', help='Number of threads for database operations', default=1, type=int) self.parser.add_argument('--threads-db', help='Number of threads for database operations', default=1, type=int)
self.parser.add_argument("--download_optional", choices=["manual", "auto"], default="manual") self.parser.add_argument('--download-optional', choices=["manual", "auto"], default="manual")
self.parser.add_argument('--coffeescript_compiler', help='Coffeescript compiler for developing', default=coffeescript, self.parser.add_argument('--lax-cert-check', action=argparse.BooleanOptionalAction, default=True, help="Enabling lax cert check allows users getting site writing priviledges by employing compromized (i.e. with leaked private keys) cert issuer. Disable for spam protection")
metavar='executable_path')
self.parser.add_argument('--tor', help='enable: Use only for Tor peers, always: Use Tor for every connection', choices=["disable", "enable", "always"], default='enable') self.parser.add_argument('--tor', help='enable: Use only for Tor peers, always: Use Tor for every connection', choices=["disable", "enable", "always"], default='enable')
self.parser.add_argument('--tor_controller', help='Tor controller address', metavar='ip:port', default='127.0.0.1:9051') self.parser.add_argument('--tor-controller', help='Tor controller address', metavar='ip:port', default='127.0.0.1:9051')
self.parser.add_argument('--tor_proxy', help='Tor proxy address', metavar='ip:port', default='127.0.0.1:9050') self.parser.add_argument('--tor-proxy', help='Tor proxy address', metavar='ip:port', default='127.0.0.1:9050')
self.parser.add_argument('--tor_password', help='Tor controller password', metavar='password') self.parser.add_argument('--tor-password', help='Tor controller password', metavar='password')
self.parser.add_argument('--tor_use_bridges', help='Use obfuscated bridge relays to avoid Tor block', action='store_true') self.parser.add_argument('--tor-use-bridges', help='Use obfuscated bridge relays to avoid Tor block', action='store_true')
self.parser.add_argument('--tor_hs_limit', help='Maximum number of hidden services in Tor always mode', metavar='limit', type=int, default=10) self.parser.add_argument('--tor-hs-limit', help='Maximum number of hidden services in Tor always mode', metavar='limit', type=int, default=10)
self.parser.add_argument('--tor_hs_port', help='Hidden service port in Tor always mode', metavar='limit', type=int, default=15441) self.parser.add_argument('--tor-hs-port', help='Hidden service port in Tor always mode', metavar='limit', type=int, default=15441)
self.parser.add_argument('--version', action='version', version='ZeroNet %s r%s' % (self.version, self.rev)) self.parser.add_argument('--repl', help='Instead of printing logs in console, drop into REPL after initialization', action='store_true')
self.parser.add_argument('--version', action='version', version=f'zeronet-conservancy {self.version_full}')
self.parser.add_argument('--end', help='Stop multi value argument parsing', action='store_true') self.parser.add_argument('--end', help='Stop multi value argument parsing', action='store_true')
return self.parser return self.parser
def loadTrackersFile(self): def loadTrackersFile(self):
if not self.trackers_file: if self.trackers_file is None:
return None return None
self.trackers = self.arguments.trackers[:] self.trackers = self.arguments.trackers[:]
@ -328,16 +474,17 @@ class Config(object):
if trackers_file.startswith("/"): # Absolute if trackers_file.startswith("/"): # Absolute
trackers_file_path = trackers_file trackers_file_path = trackers_file
elif trackers_file.startswith("{data_dir}"): # Relative to data_dir elif trackers_file.startswith("{data_dir}"): # Relative to data_dir
trackers_file_path = trackers_file.replace("{data_dir}", self.data_dir) trackers_file_path = trackers_file.replace('{data_dir}', str(self.data_dir))
else: # Relative to zeronet.py else:
trackers_file_path = self.start_dir + "/" + trackers_file # Relative to zeronet.py or something else, unsupported
raise RuntimeError(f'trackers_file should be relative to {{data_dir}} or absolute path (not {trackers_file})')
for line in open(trackers_file_path): for line in open(trackers_file_path):
tracker = line.strip() tracker = line.strip()
if "://" in tracker and tracker not in self.trackers: if "://" in tracker and tracker not in self.trackers:
self.trackers.append(tracker) self.trackers.append(tracker)
except Exception as err: except Exception as err:
print("Error loading trackers file: %s" % err) print(f'Error loading trackers file: {err}')
# Find arguments specified for current action # Find arguments specified for current action
def getActionArguments(self): def getActionArguments(self):
@ -402,6 +549,8 @@ class Config(object):
self.parseCommandline(argv, silent) # Parse argv self.parseCommandline(argv, silent) # Parse argv
self.setAttributes() self.setAttributes()
self.updatePaths()
self.createPaths()
if parse_config: if parse_config:
argv = self.parseConfig(argv) # Add arguments from config file argv = self.parseConfig(argv) # Add arguments from config file
@ -420,8 +569,21 @@ class Config(object):
self.loadTrackersFile() self.loadTrackersFile()
# Parse command line arguments def fixArgs(self, args):
"Fix old-style flags and issue a warning"
res = []
for arg in args:
if arg.startswith('--') and '_' in arg:
farg = arg.replace('_', '-')
print(f'[bold red]WARNING: using deprecated flag in command line: {arg} should be {farg}[/bold red]')
print('Support for deprecated flags might be removed in the future')
else:
farg = arg
res.append(farg)
return res
def parseCommandline(self, argv, silent=False): def parseCommandline(self, argv, silent=False):
argv = self.fixArgs(argv)
# Find out if action is specificed on start # Find out if action is specificed on start
action = self.getAction(argv) action = self.getAction(argv)
if not action: if not action:
@ -437,12 +599,19 @@ class Config(object):
self.arguments = {} self.arguments = {}
else: else:
self.arguments = self.parser.parse_args(argv[1:]) self.arguments = self.parser.parse_args(argv[1:])
if self.arguments.ui_site_port is None:
self.arguments.ui_site_port = self.arguments.ui_port + 1
if self.arguments.ui_ip_protect == 'always':
self.arguments.ui_check_cors = True
elif self.arguments.ui_ip_protect == 'off':
self.arguments.ui_check_cors = False
elif self.arguments.ui_ip_protect == 'local':
self.arguments.ui_check_cors = self.arguments.ui_ip == '127.0.0.1' or self.arguments.ui_ip == '::1'
else:
raise Exception("Wrong argparse result")
# Parse config file
def parseConfig(self, argv): def parseConfig(self, argv):
# Find config file path from parameters argv = self.fixArgs(argv)
if "--config_file" in argv:
self.config_file = argv[argv.index("--config_file") + 1]
# Load config file # Load config file
if os.path.isfile(self.config_file): if os.path.isfile(self.config_file):
config = configparser.RawConfigParser(allow_no_value=True, strict=False) config = configparser.RawConfigParser(allow_no_value=True, strict=False)
@ -453,13 +622,9 @@ class Config(object):
val = None val = None
if section != "global": # If not global prefix key with section if section != "global": # If not global prefix key with section
key = section + "_" + key key = section + "_" + key
key = key.replace('_', '-')
if key == "open_browser": # Prefer config file value over cli argument argv_extend = [f'--{key}']
while "--%s" % key in argv:
pos = argv.index("--open_browser")
del argv[pos:pos + 2]
argv_extend = ["--%s" % key]
if val: if val:
for line in val.strip().split("\n"): # Allow multi-line values for line in val.strip().split("\n"): # Allow multi-line values
argv_extend.append(line) argv_extend.append(line)
@ -489,7 +654,7 @@ class Config(object):
val = val[:] val = val[:]
if key in ("data_dir", "log_dir", "start_dir", "openssl_bin_file", "openssl_lib_file"): if key in ("data_dir", "log_dir", "start_dir", "openssl_bin_file", "openssl_lib_file"):
if val: if val:
val = val.replace("\\", "/") val = Path(val)
setattr(self, key, val) setattr(self, key, val)
def loadPlugins(self): def loadPlugins(self):
@ -591,7 +756,7 @@ class Config(object):
format = '%(name)s %(message)s' format = '%(name)s %(message)s'
if self.console_log_level == "default": if self.console_log_level == "default":
if self.silent: if self.silent or self.repl:
level = logging.ERROR level = logging.ERROR
elif self.debug: elif self.debug:
level = logging.DEBUG level = logging.DEBUG
@ -642,10 +807,6 @@ class Config(object):
except Exception as err: except Exception as err:
print("Can't change permission of %s: %s" % (self.log_dir, err)) print("Can't change permission of %s: %s" % (self.log_dir, err))
# Make warning hidden from console
logging.WARNING = 15 # Don't display warnings if not in debug mode
logging.addLevelName(15, "WARNING")
logging.getLogger('').name = "-" # Remove root prefix logging.getLogger('').name = "-" # Remove root prefix
self.error_logger = ErrorLogHandler() self.error_logger = ErrorLogHandler()
@ -657,6 +818,28 @@ class Config(object):
if file_logging: if file_logging:
self.initFileLogger() self.initFileLogger()
def tor_proxy_split(self):
if self.tor_proxy:
if ':' in config.tor_proxy:
ip, port = config.tor_proxy.rsplit(":", 1)
else:
ip = 'localhost'
port = config.tor_proxy
return ip, int(port)
else:
return 'localhost', 9050
def tor_controller_split(self):
if self.tor_controller:
if ':' in config.tor_controller:
ip, port = config.tor_controller.rsplit(":", 1)
else:
ip = 'localhost'
port = config.tor_controller
return ip, int(port)
else:
return 'localhost', 9051
class ErrorLogHandler(logging.StreamHandler): class ErrorLogHandler(logging.StreamHandler):
def __init__(self): def __init__(self):

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