├── www ├── src │ ├── components │ │ ├── user_dashboard.css │ │ ├── node_countries_chart.css │ │ ├── check_sanctions.css │ │ ├── pings_chart.css │ │ ├── storj_tx_summary.css │ │ ├── nodes_location_summary.css │ │ ├── auth.css │ │ ├── nodes_subnet_summary.css │ │ ├── help.css │ │ ├── user_dashboard.js │ │ ├── ping_my_node.css │ │ ├── rewind_control.js │ │ ├── help.js │ │ ├── auth.js │ │ ├── user_nodes.css │ │ ├── check_sanctions.js │ │ ├── search_neighbors.js │ │ ├── node_countries_chart.js │ │ ├── ping_my_node.js │ │ └── nodes_subnet_summary.js │ ├── utils │ │ ├── htm.js │ │ ├── arrays.js │ │ ├── types.js │ │ ├── preact_compat.js │ │ ├── store.js │ │ ├── elems.js │ │ ├── time.js │ │ ├── nodes.js │ │ └── dns.js │ ├── shame.js │ ├── errors.js │ ├── index.js │ ├── i18n.js │ ├── api.js │ └── main.css ├── templates │ ├── 500.html │ ├── neighbors.html │ ├── sanctions.html │ ├── ping_my_node.html │ ├── 404.html │ ├── user_dashboard.html │ ├── index.html │ └── _base.html ├── prettier.config.js ├── jsconfig.json ├── .eslintrc.js ├── package.json └── rollup.config.js ├── scripts ├── db_restore.sh ├── db_dump.sh ├── build.sh ├── daily_backup.sh ├── storjnet-proxy.service.example ├── storjnet-probe.service.example ├── storjnet-update.service.example ├── storjnet-http.service.example ├── storjnet-tgbot.service.example ├── geoip_db_load.sh ├── install.sh ├── crontab.example └── node_id_conv │ └── main.go ├── .gitignore ├── core ├── tokens.go ├── nodes_history.go ├── appconfig.go ├── ip_companies_test.go ├── nodes.go ├── users.go ├── ip_types.go ├── versions.go └── ip_companies.go ├── migrations ├── 007_versions_extra.go ├── 018_off_stats_full_nodes.go ├── 014_user_nodes_last_ping_was_ok.go ├── 004_appconfig.go ├── 026_node_stats_ip_types_asn_tops_isp.go ├── 003_versons.go ├── 016_user_nodes_details_updated_at.go ├── 027_network_company_unknown_ips.go ├── 019_node_stats_subnets.go ├── 002_user_texts.go ├── 022_node_stats_subnet_countries.go ├── 025_node_stats_countries_isp.go ├── 006_storj_token_known_addresses.go ├── 010_drop_storj_token_addresses.go ├── 012_subnet_search_index.go ├── 011_client_errors.go ├── 013_users_last_seen_at.go ├── 028_geoip_overrides.go ├── 015_tcp_quic_updated_at.go ├── 020_node_stats_ip_types.go ├── 023_partition_user_nodes_history.go ├── 009_node_stats.go ├── 008_nodes.go ├── 021_autonomous_systems_ipinfo.go ├── 024_network_companies.go ├── 005_storj_transactions.go ├── 001_init.go ├── 017_off_stats.go └── main.go ├── utils ├── tg.go ├── config.go ├── stuff.go ├── countries.go ├── worker.go ├── countries_generator.go ├── storjutils │ └── ping_proxy.go └── db.go ├── README.md ├── versions └── checker.go ├── server └── middlewares.go ├── nodes ├── loc_snap.go └── prober.go └── tgbot └── tgbot.go /www/src/components/user_dashboard.css: -------------------------------------------------------------------------------- 1 | .user-dashboard-nodes { 2 | margin: 0 8px; 3 | } 4 | -------------------------------------------------------------------------------- /www/templates/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 50Oups! 4 | 5 | -------------------------------------------------------------------------------- /www/src/utils/htm.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact' 2 | import htm from 'htm' 3 | 4 | export const html = htm.bind(h) 5 | -------------------------------------------------------------------------------- /www/src/utils/arrays.js: -------------------------------------------------------------------------------- 1 | /** @param {number} count */ 2 | export function zeroes(count) { 3 | return new Array(count).fill(0) 4 | } 5 | -------------------------------------------------------------------------------- /www/templates/neighbors.html: -------------------------------------------------------------------------------- 1 | {{define "title"}} 2 | {{.L.Loc "Neighbors" "ru" "Соседи"}} 3 | {{end}} 4 | 5 | 6 | {{define "content"}} 7 |
8 | {{end}} 9 | -------------------------------------------------------------------------------- /www/templates/sanctions.html: -------------------------------------------------------------------------------- 1 | {{define "title"}} 2 | {{.L.Loc "Sanctions" "ru" "Санкции"}} 3 | {{end}} 4 | 5 | 6 | {{define "content"}} 7 | 8 | {{end}} 9 | -------------------------------------------------------------------------------- /www/src/shame.js: -------------------------------------------------------------------------------- 1 | if (!Object.fromEntries) { 2 | Object.fromEntries = entries => { 3 | const obj = {} 4 | for (const entry of entries) obj[entry[0]] = entry[1] 5 | return obj 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /www/prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 110, 3 | useTabs: true, 4 | tabWidth: 4, 5 | semi: false, 6 | singleQuote: true, 7 | trailingComma: 'all', 8 | arrowParens: 'avoid', 9 | } 10 | -------------------------------------------------------------------------------- /scripts/db_restore.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | fname=${1:-dump.sql.gz} 3 | echo "restoring DB from $fname" 4 | 5 | zcat "$fname" | psql --set=ON_ERROR_STOP=1 --single-transaction --username=storjnet storjnet_db 6 | 7 | echo "done" 8 | -------------------------------------------------------------------------------- /www/src/components/node_countries_chart.css: -------------------------------------------------------------------------------- 1 | .nodes-count-chart .chart { 2 | height: 110px; 3 | } 4 | .node-countries-chart { 5 | height: 192px; 6 | } 7 | .node-countries-chart .legend { 8 | font-size: 10px; 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | storjnet 3 | scripts/probe_nodes/probe_nodes 4 | scripts/node_id_conv/node_id_conv 5 | scripts/update_ip_types/update_ip_types 6 | identity 7 | history 8 | dumps 9 | *.mmdb 10 | www/node_modules 11 | www/dist 12 | -------------------------------------------------------------------------------- /www/templates/ping_my_node.html: -------------------------------------------------------------------------------- 1 | {{define "title"}} 2 | {{.L.Loc "Pingdom" "ru" "Пинговальня"}} 3 | {{end}} 4 | 5 | {{define "content"}} 6 | 7 | 8 | {{end}} 9 | -------------------------------------------------------------------------------- /core/tokens.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import "time" 4 | 5 | type StorjTokenTxSummary struct { 6 | Date time.Time 7 | Preparings []float32 `pg:",array"` 8 | Payouts []float32 `pg:",array"` 9 | PayoutCounts []int32 `pg:",array"` 10 | Withdrawals []float32 `pg:",array"` 11 | } 12 | -------------------------------------------------------------------------------- /scripts/db_dump.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | fname=${1:-dump.sql.gz} 3 | echo "dumping DB to $fname" 4 | 5 | pg_dump --username=storjnet --schema=storjnet --format=p storjnet_db --clean --if-exists | grep -vP "(DROP SCHEMA|CREATE SCHEMA)" | gzip -5 --force > "$fname" 6 | 7 | echo -n "done, " 8 | du -h "$fname" | cut -f1 9 | -------------------------------------------------------------------------------- /www/src/components/check_sanctions.css: -------------------------------------------------------------------------------- 1 | .check-sanctions-form input[type='submit'] { 2 | vertical-align: top; 3 | } 4 | 5 | .check-sanctions-result-table tr:last-of-type { 6 | border: none; 7 | } 8 | 9 | .check-sanctions-result-table td.sanction { 10 | max-width: 192px; 11 | text-align: center; 12 | } 13 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | 2 | #!/bin/bash 3 | set -e 4 | base=`dirname "$0"`"/.." 5 | 6 | echo building server... 7 | cd $base 8 | go build -v 9 | 10 | cd "www" 11 | echo installing client deps... 12 | npm install --omit=dev 13 | 14 | echo cleanup... 15 | rm -rf "dist" 16 | 17 | echo building client... 18 | npm run build 19 | -------------------------------------------------------------------------------- /scripts/daily_backup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | scripts_dirname=`dirname "$0"` 3 | dumps_dirname=${1:-$scripts_dirname/../dumps} 4 | mkdir -p "$dumps_dirname" 5 | stamp=`date -Idate` 6 | "$scripts_dirname/db_dump.sh" "$dumps_dirname/dump_$stamp.sql.gz" 7 | find "$dumps_dirname" -type f ! -regex '.*/dump.*-\(01\|15\).sql.gz' ! -name "dump_$stamp.sql.gz" -exec rm {} \; 8 | -------------------------------------------------------------------------------- /scripts/storjnet-proxy.service.example: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Storjnet Pings Proxy 3 | After=network.target 4 | 5 | [Service] 6 | User=storj 7 | WorkingDirectory=/home/storj/storjnet 8 | Environment="PROXY_ENDPOINT_PATH=/proxy/endpoint/path" 9 | ExecStart=/home/storj/storjnet/storjnet ping-proxy --addr=0.0.0.0:9005 10 | Restart=on-failure 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /scripts/storjnet-probe.service.example: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Storjnet Nodes Prober 3 | After=network.target postgresql.service 4 | 5 | [Service] 6 | User=storj 7 | WorkingDirectory=/home/storj/storjnet 8 | Environment="SATELLITES=Local:identity|Proxy:/proxy/endpoint/path:9005:1.2.3.4" 9 | ExecStart=/home/storj/storjnet/storjnet probe-nodes 10 | Restart=on-failure 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /migrations/007_versions_extra.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/go-pg/migrations/v8" 4 | 5 | func init() { 6 | migrations.MustRegisterTx(func(db migrations.DB) error { 7 | return execSome(db, ` 8 | ALTER TABLE storjnet.versions ADD COLUMN extra jsonb 9 | `) 10 | }, func(db migrations.DB) error { 11 | return execSome(db, ` 12 | ALTER TABLE storjnet.versions DROP COLUMN extra 13 | `) 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /scripts/storjnet-update.service.example: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Storjnet nodes updating daemon 3 | After=network.target postgresql.service 4 | 5 | [Service] 6 | User=storj 7 | WorkingDirectory=/home/storj/storjnet 8 | Environment="SATELLITES=Local:identity|Proxy:/proxy/endpoint/path:9005:1.2.3.4" 9 | ExecStart=/home/storj/storjnet/storjnet update 10 | Restart=on-failure 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /core/nodes_history.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "time" 5 | 6 | "storj.io/common/storj" 7 | ) 8 | 9 | type UserNodeHistory struct { 10 | tableName struct{} `pg:"user_nodes_history"` 11 | RawNodeID []byte `json:"-"` 12 | NodeID storj.NodeID `json:"nodeId"` 13 | UserID int64 `json:"userId"` 14 | Date time.Time `json:"date"` 15 | Pings []uint16 `json:"pings" pg:",array"` 16 | } 17 | -------------------------------------------------------------------------------- /www/src/components/pings_chart.css: -------------------------------------------------------------------------------- 1 | .pings-chart { 2 | position: relative; 3 | height: 40px; 4 | margin-bottom: 4px; 5 | } 6 | 7 | .pings-chart .zoom-canvas { 8 | position: absolute; 9 | pointer-events: none; 10 | left: 0; 11 | bottom: 0; 12 | width: 512px; 13 | height: 64px; 14 | max-width: 100%; 15 | /* outline: 1px solid red; */ 16 | z-index: 1; 17 | } 18 | .pings-chart .zoom-canvas.touch { 19 | height: 96px; 20 | } 21 | -------------------------------------------------------------------------------- /migrations/018_off_stats_full_nodes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/go-pg/migrations/v8" 4 | 5 | func init() { 6 | migrations.MustRegisterTx(func(db migrations.DB) error { 7 | return execSome(db, ` 8 | ALTER TABLE off_node_stats ADD COLUMN full_nodes bigint; 9 | `) 10 | }, func(db migrations.DB) error { 11 | return execSome(db, ` 12 | ALTER TABLE off_node_stats DROP COLUMN full_nodes; 13 | `) 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /scripts/storjnet-http.service.example: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Storjnet HTTP server 3 | After=network.target postgresql.service 4 | 5 | [Service] 6 | User=storj 7 | WorkingDirectory=/home/storj/storjnet 8 | Environment="SATELLITES=Local:identity|Proxy:/proxy/endpoint/path:9005:1.2.3.4" 9 | ExecStart=/home/storj/storjnet/storjnet http --env=prod --addr=127.0.0.1:9004 10 | Restart=on-failure 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /migrations/014_user_nodes_last_ping_was_ok.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/go-pg/migrations/v8" 4 | 5 | func init() { 6 | migrations.MustRegisterTx(func(db migrations.DB) error { 7 | return execSome(db, ` 8 | ALTER TABLE user_nodes ADD COLUMN last_ping_was_ok bool; 9 | `) 10 | }, func(db migrations.DB) error { 11 | return execSome(db, ` 12 | ALTER TABLE user_nodes DROP COLUMN last_ping_was_ok; 13 | `) 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /migrations/004_appconfig.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/go-pg/migrations/v8" 4 | 5 | func init() { 6 | migrations.MustRegisterTx(func(db migrations.DB) error { 7 | return execSome(db, ` 8 | CREATE TABLE storjnet.appconfig ( 9 | key TEXT PRIMARY KEY, 10 | value JSONB NOT NULL 11 | ) 12 | `) 13 | }, func(db migrations.DB) error { 14 | return execSome(db, ` 15 | DROP TABLE storjnet.appconfig 16 | `) 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /www/src/components/storj_tx_summary.css: -------------------------------------------------------------------------------- 1 | .storj-tx-summary-chart .legend { 2 | right: 0; 3 | } 4 | .storj-tx-summary-chart .scale-mode-wrap { 5 | display: flex; 6 | justify-content: flex-end; 7 | flex-grow: 1000; 8 | } 9 | .storj-tx-summary-chart .scale-mode-wrap button { 10 | font: inherit; 11 | height: auto; 12 | padding: 1px 2px; 13 | margin: -2px 1px; 14 | } 15 | .storj-tx-summary-chart .scale-mode-wrap button.active { 16 | background: lightgray; 17 | } 18 | -------------------------------------------------------------------------------- /www/templates/404.html: -------------------------------------------------------------------------------- 1 | {{define "title"}}404{{end}} 2 | 3 | {{define "content"}} 4 | 19 |{{.UserText}}
15 | {{else}}
16 |
17 | {{- if .L.Is "ru"}}
18 | Сюда можно вывести произвольный текст, отправив его в теле POST-запроса на storjnet.info/api/user_texts с Basic Auth.
19 |
20 | Простую строку можно отправи так:
21 | curl --user username:password --data 'some data' https://storjnet.info/api/user_texts
22 |
23 | Выхлоп скрипта — так:
24 | sh print_stats.sh | curl -u username:password --data-binary @- https://storjnet.info/api/user_texts
25 | {{else}}
26 | You can display arbitrary text here by sending it in the POST request body to storjnet.info/api/user_texts with Basic Auth.
27 |
28 | Simple line can be sent with:
29 | curl --user username:password --data 'some data' https://storjnet.info/api/user_texts
30 |
31 | Some script output — with:
32 | sh print_stats.sh | curl -u username:password --data-binary @- https://storjnet.info/api/user_texts
33 | {{end -}}
34 |
35 | {{end}}
36 |
37 | {{else}}
38 |
39 | {{end}}
40 |
41 | {{end}}
--------------------------------------------------------------------------------
/www/src/components/help.css:
--------------------------------------------------------------------------------
1 | button.help {
2 | background: #eee;
3 | color: gray;
4 | border: none;
5 | border-radius: 50%;
6 | width: 20px;
7 | height: 20px;
8 | padding: 0;
9 | transition: background-color ease 0.1s, color ease 0.1s;
10 | }
11 | button.help:hover {
12 | background: #ddd;
13 | color: black;
14 | }
15 |
16 | button.help-line {
17 | background: none;
18 | border: none;
19 | padding: 0;
20 | text-decoration: underline;
21 | text-decoration-style: dashed;
22 | font: inherit;
23 | cursor: pointer;
24 | user-select: text;
25 | }
26 |
27 | .popup {
28 | position: fixed;
29 | display: flex;
30 | flex-direction: column;
31 | justify-content: space-around;
32 | align-items: center;
33 | width: 100%;
34 | height: 100%;
35 | background-color: rgba(0, 0, 0, 0.5);
36 | }
37 | .popup .popup-frame {
38 | position: relative;
39 | display: inline-block;
40 | background-color: white;
41 | border-radius: 2px;
42 | max-width: min(800px, 90vw);
43 | max-height: 90vh;
44 | }
45 | .popup .popup-close {
46 | position: absolute;
47 | top: 0;
48 | right: 0;
49 | width: 25px;
50 | background: none;
51 | border: none;
52 | transition: transform ease 0.1s;
53 | }
54 | .popup .popup-close:hover {
55 | transform: scale(1.2);
56 | }
57 | .popup .popup-content {
58 | padding: 3px 14px; /*10px 16px*/
59 | box-sizing: border-box;
60 | overflow-y: auto;
61 | max-height: 100%;
62 | }
63 |
--------------------------------------------------------------------------------
/www/src/errors.js:
--------------------------------------------------------------------------------
1 | window.addEventListener('error', e => {
2 | // looks like plugin error, ignoring it
3 | if (e.message === 'Script error.' && e.filename === '') return
4 | // usually, e.message already prefixed with "Uncaught Error:"
5 | const message = `${e.message} in ${e.filename}:${e.lineno}:${e.colno}`
6 | sendError(message, e.error)
7 | })
8 |
9 | window.addEventListener('unhandledrejection', e => {
10 | // in case of Promise.reject("string") error will have no message/stack; passing that "reason" as plain text
11 | const message =
12 | 'Unhandled Rejection: ' + (e.reason && e.reason.message ? e.reason.message : e.reason + '')
13 | sendError(message, e.reason)
14 | })
15 |
16 | /**
17 | * @param {string} message
18 | * @param {Error} [error]
19 | */
20 | export function sendError(message, error) {
21 | const stack = error && error.stack
22 | const body = JSON.stringify({ message, stack, url: location.href })
23 | const headers = { 'Content-Type': 'application/json' }
24 | fetch('/api/client_errors', { method: 'POST', headers, credentials: 'same-origin', body })
25 | }
26 |
27 | export function isAbortError(error) {
28 | return typeof error === 'object' && error !== null && error.name === 'AbortError'
29 | }
30 |
31 | export function onError(error) {
32 | if (isAbortError(error)) return
33 | // eslint-disable-next-line no-console
34 | console.error(error)
35 | sendError(error + '', error)
36 | }
37 |
--------------------------------------------------------------------------------
/core/ip_companies_test.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "encoding/json"
5 | "net/netip"
6 | "testing"
7 | )
8 |
9 | func Test_ipNetwork(t *testing.T) {
10 | tests := []struct {
11 | src, ipFrom, ipTo string
12 | }{
13 | {src: `"0.0.0.0 - 255.255.255.255"`, ipFrom: "0.0.0.0", ipTo: "255.255.255.255"},
14 | {src: `"0.1.2.3 - 1.2.3.4"`, ipFrom: "0.1.2.3", ipTo: "1.2.3.4"},
15 |
16 | {src: `"1.2.3.4/25"`, ipFrom: "1.2.3.0", ipTo: "1.2.3.127"},
17 | {src: `"1.2.3.128/25"`, ipFrom: "1.2.3.128", ipTo: "1.2.3.255"},
18 | {src: `"1.2.3.4/24"`, ipFrom: "1.2.3.0", ipTo: "1.2.3.255"},
19 | {src: `"1.2.3.4/23"`, ipFrom: "1.2.2.0", ipTo: "1.2.3.255"},
20 |
21 | {src: `"2001:4002:: - 2001:4003::"`, ipFrom: "2001:4002::", ipTo: "2001:4003::"},
22 | {src: `"2001:4002::/33"`, ipFrom: "2001:4002::", ipTo: "2001:4002:7fff:ffff:ffff:ffff:ffff:ffff"},
23 | {src: `"2001:4002::/32"`, ipFrom: "2001:4002::", ipTo: "2001:4002:ffff:ffff:ffff:ffff:ffff:ffff"},
24 | {src: `"2001:4002::/31"`, ipFrom: "2001:4002::", ipTo: "2001:4003:ffff:ffff:ffff:ffff:ffff:ffff"},
25 | }
26 | for _, test := range tests {
27 | var network ipNetwork
28 | if err := json.Unmarshal([]byte(test.src), &network); err != nil {
29 | t.Fatal(err)
30 | }
31 | if network.IPFrom.Compare(netip.MustParseAddr(test.ipFrom)) != 0 {
32 | t.Errorf("%s: %s != %s", test.src, network.IPFrom, test.ipFrom)
33 | }
34 | if network.IPTo.Compare(netip.MustParseAddr(test.ipTo)) != 0 {
35 | t.Errorf("%s: %s != %s", test.src, network.IPTo, test.ipTo)
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/www/src/index.js:
--------------------------------------------------------------------------------
1 | import './errors'
2 | import './shame'
3 |
4 | import './main.css'
5 |
6 | import { renderIfExists } from './utils/elems'
7 | import { StorjTxSummary } from './components/storj_tx_summary'
8 | import { NodesLocationSummary } from './components/nodes_location_summary'
9 | import { NodesCountChart } from './components/nodes_count_chart'
10 | import { RewindControl } from './components/rewind_control'
11 | import { SatsPingsCharts } from './components/pings_chart'
12 | import { PingMyNode } from './components/ping_my_node'
13 | import { AuthForm } from './components/auth'
14 | import { SearchNeighbors } from './components/search_neighbors'
15 | import { CheckSanctions } from './components/check_sanctions'
16 | import { UserDashboardNodes, UserDashboardPings } from './components/user_dashboard'
17 | import { NodesSubnetSummary } from './components/nodes_subnet_summary'
18 |
19 | renderIfExists(AuthForm, '.auth-forms')
20 | renderIfExists(RewindControl, '.rewind-control')
21 | renderIfExists(SatsPingsCharts, '.sat-nodes')
22 | renderIfExists(StorjTxSummary, '.storj-tx-summary')
23 | renderIfExists(NodesCountChart, '.nodes-count-chart')
24 | renderIfExists(NodesLocationSummary, '.nodes-location-summary')
25 | renderIfExists(NodesSubnetSummary, '.nodes-subnet-summary')
26 | renderIfExists(PingMyNode, '.ping-my-node')
27 | renderIfExists(SearchNeighbors, '.search-neighbors')
28 | renderIfExists(CheckSanctions, '.check-sanctions')
29 | renderIfExists(UserDashboardNodes, '.user-dashboard-nodes')
30 | renderIfExists(UserDashboardPings, '.user-dashboard-pings')
31 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # StorjNet
2 |
3 | ## Useful APIs
4 |
5 | ### GET /api/neighbors/\55 | ← ${prevMonthName} 56 | ${' '}${curMonthName}${' '} 57 | ${nextMonthName} → 58 |
` 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /www/src/i18n.js: -------------------------------------------------------------------------------- 1 | export const lang = document.documentElement.lang || 'en' 2 | 3 | export function L(defaultText, ...texts) { 4 | for (let i = 0; i < texts.length; i += 2) { 5 | if (texts[i] === lang) return texts[i + 1] 6 | } 7 | return defaultText 8 | } 9 | 10 | export function pluralize(val, ...words) { 11 | if (val < 0) { 12 | val = -val 13 | } 14 | let d0 = val % 10 15 | let d10 = val % 100 16 | switch (lang) { 17 | case 'ru': 18 | if (d10 === 11 || d10 === 12 || d0 === 0 || (d0 >= 5 && d0 <= 9)) { 19 | return words[2] 20 | } 21 | if (d0 >= 2 && d0 <= 4) { 22 | return words[1] 23 | } 24 | return words[0] 25 | default: 26 | if (d10 === 11 || d10 === 12 || d0 === 0 || (d0 >= 2 && d0 <= 9)) { 27 | return words[1] 28 | } 29 | return words[0] 30 | } 31 | } 32 | 33 | /** 34 | * @param {number|null} val 35 | * @param {...string} words 36 | */ 37 | L.n = function L_n(val, ...words) { 38 | return (val === null ? '...' : val.toLocaleString(lang)) + ' ' + pluralize(val || 0, ...words) 39 | } 40 | 41 | /** 42 | * @param {number|null} val 43 | * @param {string} suffix 44 | * @param {...string} words 45 | */ 46 | L.ns = function L_ns(val, suffix, ...words) { 47 | return (val === null ? '...' : val.toLocaleString(lang)) + suffix + ' ' + pluralize(val || 0, ...words) 48 | } 49 | 50 | /** 51 | * @param {number} duration 52 | * @returns {string} 53 | */ 54 | export function stringifyDuration(duration) { 55 | const days = Math.floor(duration / (24 * 3600 * 1000)) 56 | const hours = Math.floor((duration / (3600 * 1000)) % 24) 57 | const minutes = Math.floor((duration / (60 * 1000)) % 60) 58 | const seconds = Math.floor((duration / 1000) % 60) 59 | switch (lang) { 60 | case 'ru': { 61 | let res = seconds + ' с' 62 | if (minutes !== 0) res = minutes + ' мин ' + res 63 | if (hours !== 0) res = hours + ' ч ' + res 64 | if (days !== 0) res = days + ' д ' + res 65 | return res 66 | } 67 | default: { 68 | let res = seconds + ' s' 69 | if (minutes !== 0) res = minutes + ' min ' + res 70 | if (hours !== 0) res = hours + ' h ' + res 71 | if (days !== 0) res = days + ' d ' + res 72 | return res 73 | } 74 | } 75 | } 76 | /** 77 | * @param {Date|number} date 78 | * @returns {string} 79 | */ 80 | export function ago(date) { 81 | return stringifyDuration(Date.now() - +date) 82 | } 83 | -------------------------------------------------------------------------------- /migrations/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "os" 9 | "strings" 10 | "time" 11 | 12 | "github.com/go-pg/migrations/v8" 13 | "github.com/go-pg/pg/v10" 14 | ) 15 | 16 | const usageText = `This program runs command on the db. Supported commands are: 17 | - up - runs all available migrations. 18 | - down - reverts last migration. 19 | - reset - reverts all migrations. 20 | - version - prints current db version. 21 | - set_version [version] - sets db version without running migrations. 22 | Usage: 23 | go run *.go [args]11 | {{.L.Loc "My nodes" "ru" "Мои ноды"}} 12 |
13 | {{else}} 14 | 15 | {{end}} 16 |17 | {{.L.Loc "Ping nodes" "ru" "Попинговать ноды"}} 18 | {{.L.Loc "by ID and address" "ru" "по айди и адресу"}}. 19 |
20 |21 | {{.L.Loc "Count neighbors" "ru" "Посчитать соседей"}} 22 | {{.L.Loc "on /24-subnet" "ru" "в /24-подсети"}}. 23 |
24 |25 | {{.L.Loc "Check" "ru" "Проверить"}}{{.L.Loc "" "ru" ","}} 26 | {{.L.Loc "if IP is sanctioned" "ru" "не попадает ли IP под санкции"}}. 27 |
28 |33 | {{.L.Loc "Checking" "ru" "Проверка"}} 34 | {{.L.Loc "IP sanctions (in" "ru" "санкций на IP (и в"}} 35 | {{.L.Loc "nodes table" "ru" "списке нод"}} 36 | {{.L.Loc "too)" "ru" "тоже)"}}. 37 |
38 |40 | {{.L.Loc "IPs ASN and company details in" "ru" "Сведения об ASN и компаниях в"}} 41 | {{.L.Loc "nodes table" "ru" "списке нод"}}. 42 |
43 |45 | {{.L.Loc "Subnet stats" "ru" "Статистика подсетей"}}. 46 |
47 |49 | {{.L.Loc "Network size with official nodes count" "ru" "Размер сети с официальным кол-вом нод"}}. 50 |
51 |53 | {{.L.Loc "Ping nodes" "ru" "Проверка нод"}} 54 | {{.L.Loc "by QUIC too" "ru" "и через QUIC"}}. 55 |
56 |58 | {{.L.Loc "Nodes count by countries." "ru" "График нод по странам."}} 59 |
60 |
62 | {{.L.Loc "API for multi-node" "ru" "АПИ для группового"}}
63 |
64 | {{.L.Loc "neighbors counting" "ru" "подсчёта соседей"}}.
65 | {{.L.Loc "Neighbors counting in" "ru" "Подсчёт соседей в"}}
66 | {{.L.Loc "my nodes list" "ru" "списке своих нод"}}.
67 |
69 | {{.L.Loc "more" "ru" "ещё" }} 70 |
71 |${response.message}
` 62 | : html`<${IPsTable} addrs=${requestAddrs} resolvedIPs=${resolvedIPs} resp=${response} />`} 63 | <${NodeSanctionGeneralDescrPP} />` 64 | } 65 | 66 | /** @param {{addrs: string[], resolvedIPs:(string|Error)[]|null, resp: import('src/api').IPsSanctionsResponse|null}} props */ 67 | function IPsTable({ addrs, resolvedIPs, resp }) { 68 | if (addrs.length === 0) return null 69 | 70 | let maxSubdivsCount = 0 71 | if (resp) 72 | for (const ip in resp.ips) { 73 | const len = resp.ips[ip].fullInfo?.subdivisions.length 74 | if (len && len > maxSubdivsCount) maxSubdivsCount = len 75 | } 76 | 77 | return html`| IP | 80 |${L('Reason', 'ru', 'Причина')} | 81 |${L('Registration', 'ru', 'Регистрация')} | 82 |${L('Country', 'ru', 'Страна')} | 83 |${L('Region', 'ru', 'Регион')} | 84 |${L('City', 'ru', 'Город')} | 85 |
|---|---|---|---|---|---|
|
95 | ${addr}
96 | ${ip === addr
97 | ? null
98 | : ip instanceof Error
99 | ? html`${ip.message} `
100 | : html`${ip ?? ''} `}
101 | |
102 |
103 |
104 | ${sanc ? html`<${NodeSanctionDescr} sanction=${sanc} />` : null}
105 |
106 | |
107 | 108 | <${GeoLabel} item=${full?.registeredCountry} /> 109 | | 110 |<${GeoLabel} item=${full?.country} /> | 111 | ${Array(maxSubdivsCount) 112 | .fill(0) 113 | .map(i => html`<${GeoLabel} item=${full?.subdivisions[i]} /> | `)} 114 |<${GeoLabel} item=${full?.city} /> | 115 |
111 | ${count === null 112 | ? NBSP 113 | : lang === 'ru' 114 | ? html`В подсети ${pluralize(count, 'нашлась', 'нашлось', 'нашлось')}${' '} 115 | ${L.n(count, 'нода', 'ноды', 'нод')}${' '} 116 | 117 | ${pluralize(count, 'активная', 'активные', 'активных')} за последние 24 часа 118 | ` 119 | : html`${L.n(count, 'node', 'nodes')} ${pluralize(count, 'was', 'were')} found in 120 | the subnet${' '} reachable within the last 24 hours `} 121 |
122 |123 | ${sanction 124 | ? html`<${NodeSanctionDescr} sanction=${sanction} />,${' '} 125 | ${L('possible payout problems', 'ru', 'возможны проблемы с выплатами')},${' '} 126 | ${L('more info', 'ru', 'подробнее')}` 127 | : null} 128 |
129 |${isLoading ? L('Loading…', 'ru', 'Загрузка…') : null}
130 | ${logText && html`${logText}`}
131 |
31 | ${L('After ', 'ru', 'После ')}
32 | v1.30.2
33 | ${L(' nodes respond with an error like ', 'ru', ' ноды отвечают ошибкой типа ')}
34 | satellite is untrusted
35 | ${lang === 'ru'
36 | ? ' на пинги от недоверенных сателлитов. При получении такой ошибки пинг тоже будет считаться успешным.'
37 | : ' to pings from untrusted satellites. If such an error is received, the ping will also be considered successful.'}
38 |
43 | Dial — ${L(' just try to connect to node', 'ru', ' просто попытаться подключиться к ноде')} 44 | ${' '}(${L('ip:port connection', 'ru', 'коннект на ip:port')} + TLS handshake). 45 |
46 |47 | Ping — 48 | ${lang === 'ru' 49 | ? ' подключиться и отправить пинг (через сторжевый RPC).' 50 | : ' connect and send ping (via Storj RPC).'} 51 | ${' '} 52 | <${Help} letter="*" contentFunc=${getPingDetails} /> 53 |
54 | ` 55 | } 56 | 57 | export function SubnetNeighborsDescription() { 58 | return html` 59 |60 | ${L('Since traffic is ', 'ru', 'Т.к. трафик ')} 61 | 62 | ${lang === 'ru' 63 | ? 'делится между всеми нодами в /24-подсети' 64 | : 'divided between all nodes in /24-subnet'} 65 | 66 | ${lang === 'ru' 67 | ? ', лучше разносить ноды по разным подсетям или хотя бы знать, что рядом кто-то делит трафик.' 68 | : ", it's good to keep nodes on different subnets or at least know if someone is sharing traffic."} 69 |
70 |71 | ${lang === 'ru' 72 | ? `Некоторые ноды (особенно новые) могут не учитываться. ` + 73 | `Если есть сомнения, лучше проверить свою подсеть вручную (например Nmap'ом).` 74 | : 'Some nodes (especially new ones) may not be found. ' + 75 | 'If in doubt, better check your subnet manually (e.g. with Nmap).'} 76 |
77 | ` 78 | } 79 | 80 | /** @param {{sanction: {reason: string, detail: string}}} props */ 81 | export function NodeSanctionDescr({ sanction }) { 82 | const reason = 83 | sanction?.reason === 'REGISTRATION_COUNTRY' 84 | ? L('IP registration country', 'ru', `Страна регистрации IP${NBHYP}адреса`) 85 | : sanction?.reason === 'LOCATION_REGION' 86 | ? L('IP location', 'ru', `Местоположение IP${NBHYP}адреса`) 87 | : (sanction?.reason ?? '').toLowerCase().replace(/_/g, ' ') 88 | return html`${reason}: ${sanction?.detail}` 89 | } 90 | 91 | export function NodeSanctionGeneralDescrPP() { 92 | const threadUrl = 'https://forum.storj.io/t/missing-payouts-because-node-is-in-a-sanctioned-country' 93 | const logicUrl = 94 | 'https://forum.storj.io/t/missing-payouts-because-node-is-in-a-sanctioned-country/27400/51' 95 | 96 | return html`97 | ${lang === 'ru' 98 | ? html`Судя по теме на форуме, ноды с адресами в подсанкционных 99 | странах/регионах не получают оплату.` 100 | : html`According to the forum thread, nodes with addresses in 101 | sanctioned countries/regions do not receive payouts.`} 102 | ${' '} 103 | ${lang === 'ru' 104 | ? html`Хотя полного списка таких адресов нет, в${' '} 105 | одном из сообщений описана проверка на санкционность, 106 | аналогичная используется и здесь.` 107 | : html`Although there is no complete list of such addresses,${' '} 108 | one of the posts describes a sanction check, similar one is 109 | used here.`} 110 |
111 |112 | ${lang === 'ru' 113 | ? `Полный и актуальный исходный код проверки неизвестен. Если есть сомнения, можно воспользоваться скриптом из первого сообщения ветки или обатиться в поддержку.` 114 | : `Complete and up-to-date source code of the check is unknown. If you have doubts, you can use the script from the thread's first post or contact support.`} 115 |
` 116 | } 117 | -------------------------------------------------------------------------------- /www/src/components/node_countries_chart.js: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'preact/hooks' 2 | import { apiReq } from 'src/api' 3 | import { onError } from 'src/errors' 4 | import { 5 | CanvasExt, 6 | drawLabeledVScaleLeftLine, 7 | drawLineStepped, 8 | drawMonthDays, 9 | getArrayMaxValue, 10 | LegendItem, 11 | RectCenter, 12 | roundRange, 13 | View, 14 | } from 'src/utils/charts' 15 | import { delayedRedraw, useResizeEffect } from 'src/utils/elems' 16 | import { html } from 'src/utils/htm' 17 | import { DAY_DURATION, toISODateString, useHashInterval } from 'src/utils/time' 18 | 19 | import './node_countries_chart.css' 20 | import { lang } from 'src/i18n' 21 | 22 | /** @typedef {{name:string, a3:string, counts:Uint16Array}} CountryItem */ 23 | 24 | function c(str, n) { 25 | return str.charCodeAt(n) || 0 26 | } 27 | 28 | /** @param {string} name */ 29 | function name2col(name) { 30 | // const h = (3600 + 90 - 7 * c(name, 0) - 3 * c(name, 1) + 11 * c(name, 2)) % 360 31 | // let s = 90 - 50 * ((c(name, 0) / 7) % 1) ** 2 32 | // let l = 55 - 25 * ((c(name, 0) / 11) % 1) ** 2 33 | // l = Math.min(l, 50 - 15 * Math.max(0, Math.sin(((h - 45) / 180) * Math.PI)) ** 2) 34 | // return `hsl(${h} ${s}% ${l}%)` 35 | const sum = c(name, 0) + c(name, 1) + c(name, 2) 36 | const max = Math.max(c(name, 0), c(name, 1), c(name, 2)) 37 | let r = (max * 1.0031 * c(name, 0) + sum * c(name, 2) + max * c(name, 1)) % 256 | 0 38 | let g = (max * 1.0072 * c(name, 1) + sum * c(name, 0) + max * c(name, 2)) % 256 | 0 39 | let b = (max * 1.0025 * c(name, 2) + sum * c(name, 1) + max * c(name, 0)) % 256 | 0 40 | 41 | const lum = r * 0.21 + g * 0.72 + b * 0.07 42 | let dLum = 0 43 | if (lum > 150) dLum = -130 44 | if (lum < 120) dLum = 120 - lum * 0.75 45 | if (dLum !== 0) { 46 | r = Math.max(0, r + dLum * 0.21) | 0 47 | g = Math.max(0, g + dLum * 0.72) | 0 48 | b = Math.max(0, b + dLum * 0.07) | 0 49 | } 50 | return `rgb(${r},${g},${b})` 51 | } 52 | 53 | export function NodeCountriesChart() { 54 | const canvasExt = useRef(new CanvasExt()).current 55 | const legendBoxRef = useRef(/**@type {HTMLDivElement|null}*/ (null)) 56 | const [startDate, endDate] = useHashInterval() 57 | 58 | const [data, setData] = useState(/**@type {{startStamp:number, countries:CountryItem[]}|null}*/ (null)) 59 | 60 | const rect = useRef(new RectCenter({ left: 0, right: 0, top: 31, bottom: 11 })).current 61 | const view = useRef(new View({ startStamp: 0, endStamp: 0, bottomValue: 0, topValue: 1 })).current 62 | 63 | const onRedraw = useCallback(() => { 64 | let { rc } = canvasExt 65 | 66 | if (!canvasExt.created() || rc === null) return 67 | canvasExt.resize() 68 | 69 | if (legendBoxRef.current) rect.top = legendBoxRef.current.getBoundingClientRect().height + 1 70 | rect.update(canvasExt.cssWidth, canvasExt.cssHeight) 71 | view.updateStamps(+startDate, +endDate + DAY_DURATION) 72 | 73 | canvasExt.clear() 74 | rc.save() 75 | rc.scale(canvasExt.pixelRatio, canvasExt.pixelRatio) 76 | rc.lineWidth = 1.2 77 | 78 | if (data !== null) { 79 | const { startStamp: start, countries } = data 80 | const step = 3600 * 1000 81 | for (const country of countries) { 82 | const col = name2col(country.a3) 83 | drawLineStepped(rc, rect, view, country.counts, start, step, col, true, false) //value2yLog 84 | } 85 | } 86 | 87 | const textCol = 'black' 88 | const lineCol = 'rgba(0,0,0,0.08)' 89 | const midVal = (view.bottomValue + view.topValue) / 2 90 | rc.textAlign = 'left' 91 | rc.textBaseline = 'bottom' 92 | drawLabeledVScaleLeftLine(rc, rect, view, view.bottomValue, textCol, null, 0, null) 93 | rc.textBaseline = 'middle' 94 | drawLabeledVScaleLeftLine(rc, rect, view, midVal, textCol, lineCol, 0, null) 95 | rc.textBaseline = 'top' 96 | drawLabeledVScaleLeftLine(rc, rect, view, view.topValue, textCol, lineCol, 0, null) 97 | 98 | drawMonthDays(canvasExt, rect, view, {}) 99 | 100 | rc.restore() 101 | }, [startDate, endDate, data]) 102 | const requestRedraw = useMemo(() => delayedRedraw(onRedraw), [onRedraw]) 103 | 104 | useEffect(() => { 105 | const abortController = new AbortController() 106 | apiReq('GET', `/api/nodes/countries`, { 107 | data: { start_date: toISODateString(startDate), end_date: toISODateString(endDate), lang }, 108 | signal: abortController.signal, 109 | }) 110 | .then(r => r.arrayBuffer()) 111 | .then(arrayBuf => { 112 | const uint32Buf = new Uint32Array(arrayBuf, 0, 2) 113 | const startStamp = uint32Buf[0] * 1000 114 | const countsLength = uint32Buf[1] 115 | 116 | /** @type {CountryItem[]} */ 117 | const countries = [] 118 | 119 | const textDec = new TextDecoder() 120 | const buf = new Uint8Array(arrayBuf) 121 | let pos = 8 122 | let maxCount = 0 123 | while (pos < buf.length) { 124 | const a3AndNameLen = buf[pos] 125 | const a3AndName = textDec.decode(new Uint8Array(arrayBuf, pos + 1, a3AndNameLen)) 126 | const [a3, name] = a3AndName.split('|') 127 | let countsOffset = 1 + a3AndNameLen 128 | if (countsOffset % 2 === 1) countsOffset += 1 129 | pos += countsOffset 130 | const counts = new Uint16Array(arrayBuf, pos, countsLength) 131 | pos += 2 * countsLength 132 | countries.push({ name, a3, counts }) 133 | maxCount = Math.max(maxCount, getArrayMaxValue(counts)) 134 | } 135 | 136 | countries.sort((a, b) => b.counts[countsLength - 1] - a.counts[countsLength - 1]) 137 | 138 | setData({ startStamp, countries }) 139 | view.updateLimits(...roundRange(0, maxCount)) 140 | }) 141 | .catch(onError) 142 | return () => abortController.abort() 143 | }, [startDate, endDate]) 144 | 145 | useLayoutEffect(() => { 146 | requestRedraw() 147 | }, [requestRedraw]) 148 | 149 | useResizeEffect(requestRedraw, [requestRedraw]) 150 | 151 | return html` 152 |193 | ${lang === 'ru' ? 'Будут проверены и TCP, и ' : 'Will try both TCP and '} 194 | QUIC. 195 |
196 | 212 |${logText}
213 | `
214 | }
215 | }
216 |
--------------------------------------------------------------------------------
/core/versions.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io"
7 | "net/http"
8 |
9 | "github.com/ansel1/merry"
10 | "github.com/blang/semver"
11 | "github.com/go-pg/pg/v10"
12 | "github.com/rs/zerolog/log"
13 | )
14 |
15 | type prefixedVersion semver.Version
16 |
17 | func (v *prefixedVersion) UnmarshalJSON(data []byte) (err error) {
18 | var versionString string
19 | if err = json.Unmarshal(data, &versionString); err != nil {
20 | return merry.Wrap(err)
21 | }
22 | if versionString[0] == 'v' {
23 | versionString = versionString[1:]
24 | }
25 | sv, err := semver.Parse(versionString)
26 | if err != nil {
27 | return merry.Wrap(err)
28 | }
29 | *v = prefixedVersion(sv)
30 | return nil
31 | }
32 |
33 | var GitHubOAuthToken = ""
34 |
35 | type VersionChecker interface {
36 | Key() string
37 | FetchPrevVersion() error
38 | FetchCurVersion() error
39 | VersionHasChanged() bool
40 | SaveCurVersion() error
41 | DebugVersions() (string, string)
42 | MessageNew() string
43 | MessageCur() string
44 | }
45 |
46 | type CurVersionChecker interface {
47 | Key() string
48 | FetchCurVersion() error
49 | MessageCur() string
50 | }
51 |
52 | type Cursor string
53 |
54 | func (c Cursor) String() string {
55 | if len(c) > 7 {
56 | c = c[:3] + "…" + c[len(c)-3:]
57 | }
58 | return string(c)
59 | }
60 |
61 | func (c Cursor) IsFinal() bool {
62 | for _, char := range c {
63 | if char != 'f' {
64 | return false
65 | }
66 | }
67 | return len(c) > 0
68 | }
69 |
70 | type VersionWithCursor struct {
71 | Version semver.Version
72 | Cursor Cursor
73 | }
74 |
75 | func (v VersionWithCursor) VString() string {
76 | return fmt.Sprintf("v%s, cursor: %s", v.Version, v.Cursor)
77 | }
78 |
79 | type StrojIoVersionChecker struct {
80 | db *pg.DB
81 | prevVersion VersionWithCursor
82 | curVersion VersionWithCursor
83 | }
84 |
85 | func (c *StrojIoVersionChecker) Key() string {
86 | return "StorjIo:storagenode"
87 | }
88 |
89 | func (c *StrojIoVersionChecker) FetchPrevVersion() error {
90 | _, err := c.db.QueryOne(&c.prevVersion,
91 | `SELECT version, extra->>'cursor' AS cursor FROM versions WHERE kind = ? ORDER BY created_at DESC LIMIT 1`,
92 | c.Key())
93 | if err != nil && err != pg.ErrNoRows {
94 | return merry.Wrap(err)
95 | }
96 | return nil
97 | }
98 |
99 | func (c *StrojIoVersionChecker) SaveCurVersion() error {
100 | _, err := c.db.Exec(`INSERT INTO versions (kind, version, extra) VALUES (?, ?, json_build_object('cursor', ?))`,
101 | c.Key(), c.curVersion.Version, string(c.curVersion.Cursor))
102 | return merry.Wrap(err)
103 | }
104 |
105 | func (c *StrojIoVersionChecker) FetchCurVersion() error {
106 | resp, err := http.Get("https://version.storj.io/")
107 | if err != nil {
108 | return merry.Wrap(err)
109 | }
110 | defer resp.Body.Close()
111 | params := &struct {
112 | Processes struct {
113 | Storagenode struct {
114 | Suggested struct{ Version semver.Version }
115 | Rollout struct{ Cursor string }
116 | }
117 | }
118 | }{}
119 | if err := json.NewDecoder(resp.Body).Decode(¶ms); err != nil {
120 | return merry.Wrap(err)
121 | }
122 | c.curVersion.Version = params.Processes.Storagenode.Suggested.Version
123 | c.curVersion.Cursor = Cursor(params.Processes.Storagenode.Rollout.Cursor)
124 | return nil
125 | }
126 |
127 | func (c *StrojIoVersionChecker) VersionHasChanged() bool {
128 | return !c.curVersion.Version.Equals(c.prevVersion.Version) ||
129 | (c.curVersion.Cursor != c.prevVersion.Cursor && c.curVersion.Cursor.IsFinal())
130 | }
131 |
132 | func (c *StrojIoVersionChecker) DebugVersions() (string, string) {
133 | return c.prevVersion.VString(), c.curVersion.VString()
134 | }
135 |
136 | func (c *StrojIoVersionChecker) MessageNew() string {
137 | if c.curVersion.Version.Equals(c.prevVersion.Version) && c.curVersion.Cursor.IsFinal() {
138 | return fmt.Sprintf("Финальный курсор *%s* (v%s) на version.storj.io",
139 | c.curVersion.Cursor, c.curVersion.Version)
140 | }
141 | return fmt.Sprintf("Новая версия *v%s* (cursor: %s)\nРекомендуемая для нод на version.storj.io",
142 | c.curVersion.Version, c.curVersion.Cursor)
143 | }
144 |
145 | func (c *StrojIoVersionChecker) MessageCur() string {
146 | return fmt.Sprintf("%s (version.storj.io)", c.curVersion.VString())
147 | }
148 |
149 | type GitHubVersionChecker struct {
150 | db *pg.DB
151 | prevVersion semver.Version
152 | curVersion semver.Version
153 | }
154 |
155 | func (c *GitHubVersionChecker) Key() string {
156 | return "GitHub:latest"
157 | }
158 |
159 | func (c *GitHubVersionChecker) FetchPrevVersion() error {
160 | _, err := c.db.QueryOne(pg.Scan(&c.prevVersion),
161 | `SELECT version FROM versions WHERE kind = ? ORDER BY created_at DESC LIMIT 1`, c.Key())
162 | if err != nil && err != pg.ErrNoRows {
163 | return merry.Wrap(err)
164 | }
165 | return nil
166 | }
167 |
168 | func (c *GitHubVersionChecker) SaveCurVersion() error {
169 | _, err := c.db.Exec(`INSERT INTO versions (kind, version) VALUES (?, ?)`, c.Key(), c.curVersion)
170 | return merry.Wrap(err)
171 | }
172 |
173 | func (c *GitHubVersionChecker) FetchCurVersion() error {
174 | req, err := http.NewRequest("GET", "https://api.github.com/repos/storj/storj/releases/latest", nil)
175 | if err != nil {
176 | return merry.Wrap(err)
177 | }
178 | if GitHubOAuthToken != "" {
179 | req.Header.Set("Authorization", "token "+GitHubOAuthToken)
180 | }
181 | resp, err := http.DefaultClient.Do(req)
182 | if err != nil {
183 | return merry.Wrap(err)
184 | }
185 | defer resp.Body.Close()
186 | params := &struct {
187 | Name string
188 | TagName prefixedVersion `json:"tag_name"`
189 | }{}
190 | // if err := json.NewDecoder(resp.Body).Decode(¶ms); err != nil {
191 | // return merry.Wrap(err)
192 | // }
193 | buf, err := io.ReadAll(resp.Body)
194 | if err != nil {
195 | return merry.Wrap(err)
196 | }
197 | if err := json.Unmarshal(buf, ¶ms); err != nil {
198 | return merry.Wrap(err)
199 | }
200 | if semver.Version(params.TagName).Equals(semver.Version{}) {
201 | log.Warn().Str("resp", string(buf)).Msg("version is zero")
202 | return merry.New("version is zero")
203 | }
204 | c.curVersion = semver.Version(params.TagName)
205 | return nil
206 | }
207 |
208 | func (c *GitHubVersionChecker) VersionHasChanged() bool {
209 | // GitHub sometimes starts returning a previous version for a short period.
210 | // So ignoring the version downgrade here.
211 | return c.curVersion.GT(c.prevVersion)
212 | // return !c.curVersion.Equals(c.prevVersion)
213 | }
214 |
215 | func (c *GitHubVersionChecker) DebugVersions() (string, string) {
216 | return "v" + c.prevVersion.String(), "v" + c.curVersion.String()
217 | }
218 |
219 | func (c *GitHubVersionChecker) MessageNew() string {
220 | return fmt.Sprintf("Новый релиз *v%s*\nНа [ГитХабе](https://github.com/storj/storj/releases/tag/v%s), с ченджлогом и бинарниками.", c.curVersion, c.curVersion)
221 | }
222 |
223 | func (c *GitHubVersionChecker) MessageCur() string {
224 | return fmt.Sprintf("v%s ([GitHub](https://github.com/storj/storj/releases/tag/v%s))", c.curVersion, c.curVersion)
225 | }
226 |
227 | func MakeCurVersionCheckers() []CurVersionChecker {
228 | return []CurVersionChecker{&StrojIoVersionChecker{}, &GitHubVersionChecker{}}
229 | }
230 |
231 | func MakeVersionCheckers(db *pg.DB) []VersionChecker {
232 | return []VersionChecker{&StrojIoVersionChecker{db: db}, &GitHubVersionChecker{db: db}}
233 | }
234 |
--------------------------------------------------------------------------------
/nodes/prober.go:
--------------------------------------------------------------------------------
1 | package nodes
2 |
3 | import (
4 | "context"
5 | "storjnet/utils"
6 | "storjnet/utils/storjutils"
7 | "strconv"
8 | "strings"
9 | "sync"
10 | "sync/atomic"
11 | "time"
12 |
13 | "github.com/ansel1/merry"
14 | "github.com/go-pg/pg/v10"
15 | "github.com/rs/zerolog/log"
16 | "storj.io/common/storj"
17 | )
18 |
19 | const (
20 | nodesUpdateInterval = `INTERVAL '8 minutes'`
21 | noNodesPauseDuraton = 30 * time.Second
22 | probeRoutinesCount = 64
23 | )
24 |
25 | type ProbeNode struct {
26 | RawID []byte
27 | ID storj.NodeID
28 | IPAddr string
29 | Port uint16
30 | }
31 | type ProbeNodeErr struct {
32 | Node *ProbeNode
33 | TCPErr error
34 | QUICErr error
35 | }
36 |
37 | func errIsKnown(err error) bool {
38 | msg := err.Error()
39 | return strings.HasPrefix(msg, "rpc: context deadline exceeded") ||
40 | (strings.HasPrefix(msg, "rpc: dial tcp ") &&
41 | (strings.Contains(msg, ": connect: connection refused") ||
42 | strings.Contains(msg, ": connect: no route to host") ||
43 | strings.Contains(msg, ": i/o timeout"))) ||
44 | (strings.HasPrefix(msg, "rpc: socks connect") &&
45 | (strings.HasSuffix(msg, "connection refused") ||
46 | strings.HasSuffix(msg, "host unreachable") ||
47 | strings.Contains(msg, ": i/o timeout"))) ||
48 | strings.HasPrefix(msg, "rpc: tls peer certificate verification error: tlsopts: peer ID did not match requested ID")
49 | }
50 |
51 | func probeWithTimeout(sats storjutils.Satellites, nodeID storj.NodeID, address string, mode storjutils.SatMode) (storjutils.Satellite, error) {
52 | sat, err := sats.DialAndClose(address, nodeID, mode, 5*time.Second)
53 | return sat, merry.Wrap(err)
54 | }
55 | func probe(sats storjutils.Satellites, node *ProbeNode) (tcpSat, quicSat storjutils.Satellite, tcpErr error, quicErr error) {
56 | address := node.IPAddr + ":" + strconv.Itoa(int(node.Port))
57 | wg := sync.WaitGroup{}
58 |
59 | wg.Add(2)
60 | go func() {
61 | tcpSat, tcpErr = probeWithTimeout(sats, node.ID, address, storjutils.SatModeTCP)
62 | wg.Done()
63 | }()
64 | go func() {
65 | quicSat, quicErr = probeWithTimeout(sats, node.ID, address, storjutils.SatModeQUIC)
66 | wg.Done()
67 | }()
68 |
69 | wg.Wait()
70 | return
71 | }
72 |
73 | func startOldNodesLoader(db *pg.DB, nodesChan chan *ProbeNode, chunkSize int) utils.Worker {
74 | worker := utils.NewSimpleWorker(1)
75 |
76 | go func() {
77 | defer worker.Done()
78 | for {
79 | nodes := make([]*ProbeNode, chunkSize)
80 | err := db.RunInTransaction(context.Background(), func(tx *pg.Tx) error {
81 | _, err := tx.Query(&nodes, `
82 | SELECT id AS raw_id, ip_addr, port FROM nodes
83 | WHERE checked_at IS NULL
84 | OR (checked_at < NOW() - `+nodesUpdateInterval+`
85 | AND greatest(updated_at, last_received_from_sat_at) > NOW() - INTERVAL '7 days')
86 | ORDER BY checked_at ASC NULLS FIRST
87 | LIMIT ?
88 | FOR UPDATE`, chunkSize)
89 | if err != nil {
90 | return merry.Wrap(err)
91 | }
92 | if len(nodes) == 0 {
93 | return nil
94 | }
95 | for _, node := range nodes {
96 | node.ID, err = storj.NodeIDFromBytes(node.RawID)
97 | if err != nil {
98 | return merry.Wrap(err)
99 | }
100 | }
101 | nodeIDs := make(storj.NodeIDList, len(nodes))
102 | for i, node := range nodes {
103 | nodeIDs[i] = node.ID
104 | }
105 | _, err = tx.Exec(`UPDATE nodes SET checked_at = NOW() WHERE id IN (?)`, pg.In(nodeIDs))
106 | return merry.Wrap(err)
107 | })
108 | if err != nil {
109 | worker.AddError(err)
110 | return
111 | }
112 |
113 | log.Info().Int("IDs count", len(nodes)).Msg("PROBE:OLD")
114 | if len(nodes) == 0 {
115 | time.Sleep(noNodesPauseDuraton)
116 | }
117 | for _, node := range nodes {
118 | nodesChan <- node
119 | }
120 | }
121 | }()
122 | return worker
123 | }
124 |
125 | func startNodesProber(nodesInChan chan *ProbeNode, nodesOutChan chan *ProbeNodeErr, routinesCount int) utils.Worker {
126 | worker := utils.NewSimpleWorker(routinesCount)
127 |
128 | sats, err := storjutils.SatellitesSetUpFromEnv()
129 | if err != nil {
130 | worker.AddError(err)
131 | return worker
132 | }
133 |
134 | stamp := time.Now().Unix()
135 | countTotal := int64(0)
136 | countOk := int64(0)
137 | countTCPProxyOk := int64(0)
138 | countQUICProxyOk := int64(0)
139 | countErr := int64(0)
140 | for i := 0; i < routinesCount; i++ {
141 | go func() {
142 | defer worker.Done()
143 | for node := range nodesInChan {
144 | tcpSat, quicSat, tcpErr, quicErr := probe(sats, node)
145 | if tcpErr != nil && quicErr != nil {
146 | atomic.AddInt64(&countErr, 1)
147 | if !errIsKnown(tcpErr) {
148 | log.Info().Str("id", node.ID.String()).Msg(tcpErr.Error())
149 | }
150 | } else {
151 | if tcpSat != nil && tcpSat.UsesProxy() {
152 | atomic.AddInt64(&countTCPProxyOk, 1)
153 | }
154 | if quicSat != nil && quicSat.UsesProxy() {
155 | atomic.AddInt64(&countQUICProxyOk, 1)
156 | }
157 | atomic.AddInt64(&countOk, 1)
158 | nodesOutChan <- &ProbeNodeErr{Node: node, TCPErr: tcpErr, QUICErr: quicErr}
159 | }
160 |
161 | if atomic.AddInt64(&countTotal, 1)%100 == 0 {
162 | log.Info().
163 | Int64("total", countTotal).
164 | Int64("ok", countOk).Int64("tcpProxyOk", countTCPProxyOk).Int64("quicProxyOk", countQUICProxyOk).
165 | Int64("err", countErr).
166 | Float64("rpm", float64(countTotal)/float64(time.Now().Unix()-stamp)*60).
167 | Msg("PROBE:GET")
168 | }
169 | }
170 | }()
171 | }
172 | return worker
173 | }
174 |
175 | func startPingedNodesSaver(db *pg.DB, nodesChan chan *ProbeNodeErr, chunkSize int) utils.Worker {
176 | worker := utils.NewSimpleWorker(1)
177 | nodesChanI := make(chan interface{}, 16)
178 |
179 | go func() {
180 | for node := range nodesChan {
181 | nodesChanI <- node
182 | }
183 | close(nodesChanI)
184 | }()
185 |
186 | go func() {
187 | defer worker.Done()
188 | err := utils.SaveChunked(db, chunkSize, nodesChanI, func(tx *pg.Tx, items []interface{}) error {
189 | ids := make([]storj.NodeID, len(items))
190 | tcpErrIDs := make([]storj.NodeID, 0, len(items))
191 | quicErrIDs := make([]storj.NodeID, 0, len(items))
192 | for i, nodeI := range items {
193 | n := nodeI.(*ProbeNodeErr)
194 | ids[i] = n.Node.ID
195 | if n.TCPErr != nil {
196 | tcpErrIDs = append(tcpErrIDs, n.Node.ID)
197 | }
198 | if n.QUICErr != nil {
199 | quicErrIDs = append(quicErrIDs, n.Node.ID)
200 | }
201 | }
202 | _, err := tx.Exec(`
203 | UPDATE nodes SET
204 | updated_at = NOW(),
205 | tcp_updated_at = CASE WHEN id = any(ARRAY[?]::bytea[]) THEN tcp_updated_at ELSE NOW() END,
206 | quic_updated_at = CASE WHEN id = any(ARRAY[?]::bytea[]) THEN quic_updated_at ELSE NOW() END
207 | WHERE id IN (?)`,
208 | pg.In(tcpErrIDs), pg.In(quicErrIDs), pg.In(ids))
209 | if err != nil {
210 | return merry.Wrap(err)
211 | }
212 | return nil
213 | })
214 | log.Info().Msg("PROBE:SAVE:DONE")
215 | if err != nil {
216 | worker.AddError(err)
217 | }
218 | }()
219 | return worker
220 | }
221 |
222 | func StartProber() error {
223 | db := utils.MakePGConnection()
224 | nodesInChan := make(chan *ProbeNode, 32)
225 | nodesOutChan := make(chan *ProbeNodeErr, 32)
226 |
227 | workers := []utils.Worker{
228 | startOldNodesLoader(db, nodesInChan, 128),
229 | startNodesProber(nodesInChan, nodesOutChan, probeRoutinesCount),
230 | startPingedNodesSaver(db, nodesOutChan, 32),
231 | }
232 |
233 | iter := 0
234 | for {
235 | for _, worker := range workers {
236 | if err := worker.PopError(); err != nil {
237 | return err
238 | }
239 | }
240 |
241 | iter += 1
242 | if iter%5 == 0 {
243 | log.Info().Int("in_chan", len(nodesInChan)).Int("out_chan", len(nodesOutChan)).Msg("PROBE:STAT")
244 | }
245 |
246 | time.Sleep(time.Second)
247 | }
248 | }
249 |
--------------------------------------------------------------------------------
/www/src/components/nodes_subnet_summary.js:
--------------------------------------------------------------------------------
1 | import { memo } from 'src/utils/preact_compat'
2 | import { html } from 'src/utils/htm'
3 | import { L, lang } from 'src/i18n'
4 | import { useCallback, useEffect, useMemo, useState } from 'preact/hooks'
5 | import { apiReq } from 'src/api'
6 | import { toISODateString, useHashInterval } from 'src/utils/time'
7 | import { onError } from 'src/errors'
8 | import { zeroes } from 'src/utils/arrays'
9 |
10 | import './nodes_subnet_summary.css'
11 | import { THINSP } from 'src/utils/elems'
12 |
13 | const SIZES_STATS_COUNTS = [1, 2, 3, 10, 100]
14 |
15 | /**
16 | * @typedef {{
17 | * subnetsCount: number,
18 | * subnetsTop: {subnet:string, size:number}[],
19 | * subnetSizes: {size:number, count:number}[],
20 | * ipTypes: {type:string, count:number, asnTop:{name:string, count:number}[]}[],
21 | * }} NodesSubnetsSummaryResponse
22 | */
23 |
24 | const NodesSummary = memo(function NodesSummary() {
25 | const [stats, setStats] = useState(/** @type {NodesSubnetsSummaryResponse | null} */ (null))
26 | const [, endDate] = useHashInterval()
27 |
28 | useEffect(() => {
29 | const abortController = new AbortController()
30 |
31 | apiReq('GET', `/api/nodes/subnet_summary`, {
32 | data: { end_date: toISODateString(endDate) },
33 | signal: abortController.signal,
34 | })
35 | .then(stats => {
36 | setStats(stats)
37 | })
38 | .catch(onError)
39 |
40 | return () => abortController.abort()
41 | }, [endDate])
42 |
43 | const subnetsCount = stats ? stats.subnetsCount : 0
44 |
45 | return html`
46 | 52 | ${lang === 'ru' 53 | ? `Ноды запущены как минимум в ${L.n(subnetsCount, 'подсети', 'подсетях', 'подсетях')} /24.` 54 | : `Nodes are running in at least ${L.n(subnetsCount, 'subnet', 'subnets')} /24.`} 55 |
56 | ` 57 | }) 58 | 59 | /** @param {{subnetsTop: undefined | NodesSubnetsSummaryResponse['subnetsTop']}} props */ 60 | function NodesSubnetsSummaryTable({ subnetsTop }) { 61 | return html`| ${L('#', 'ru', '№')} | 65 |${L('Subnet', 'ru', 'Подсеть')} | 66 |
67 | ${L('Nodes', 'ru', 'Нод')}
68 | ${L('in subnet', 'ru', 'в подсети')}
69 | |
70 |
| ${i + 1} | 77 |${L('loading…', 'ru', 'загрузка…')} | 78 |… | 79 |
| ${i + 1} | 85 |<${Subnet} subnet=${item.subnet} /> | 86 |${item.size} | 87 |
| 91 | | … | 92 |93 | |
|
134 | ${L('Nodes', 'ru', 'Нод')}
135 | ${L('in subnet', 'ru', 'в подсети')}
136 | |
137 |
138 | ${L('Count', 'ru', 'Кол-во')}
139 | ${L('of subnets', 'ru', 'подсетей')}
140 | |
141 | |
| ${L('loading…', 'ru', 'загрузка…')} | 149 |||
| ${item.label} | 155 |${item.count} | 156 ||
| 164 | 165 | | 166 | `} 167 |||
|
192 | ${Object.keys(expanded).length > 0 &&
193 | html`${L('Name', 'ru', 'Название')}
194 | ${L('of AS', 'ru', "AS'ки")} `}
195 | |
196 |
197 | ${L('Type', 'ru', 'Тип')}
198 | ${L('of IP-addr', 'ru', 'IP-адреса')}
199 | |
200 |
201 | ${L('Nodes', 'ru', 'Кол-во')}
202 | ${L('count', 'ru', 'нод')}
203 | |
204 |
| ${L('loading…', 'ru', 'загрузка…')} | 211 |||
| 218 | | 219 | 222 | | 223 |${item.count} | 224 |
| ${x.name} | 230 |${x.count} | 231 ||
| 237 | | … | 238 |239 | |