diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index 9ddcbeae7..000000000 --- a/.appveyor.yml +++ /dev/null @@ -1,36 +0,0 @@ -version: "{build}" - -environment: - matrix: - # For regular jobs, such as push, pr and etc. - - job_name: Deploy - appveyor_build_worker_image: ubuntu2004 - -for: - - # Docker Deploy (Master) - skip_tags: true - build: off - matrix: - only: - - job_name: DockerDeployMaster - branches: - only: - - master - before_deploy: - - ./ci_scripts/docker-push.sh -t master -p - deploy_script: - - echo "Master Docker Push Complete!" - - - # Docker Deploy (Develop) - skip_tags: true - build: off - matrix: - only: - - job_name: DockerDeployDevelop - branches: - only: - - develop - before_deploy: - - ./ci_scripts/docker-push.sh -t develop -p - deploy_script: - - echo "Develop Docker Push Complete!" \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0ce603170..683687fd0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -53,7 +53,7 @@ jobs: - name: Install Requirements shell: pwsh run: | - Invoke-WebRequest "https://github.com/goreleaser/goreleaser/releases/download/v1.8.3/goreleaser_Windows_x86_64.zip" -o goreleaser.zip + Invoke-WebRequest "https://github.com/goreleaser/goreleaser/releases/download/v1.8.3/goreleaser_Windows_x86_64.zip" -OutFile goreleaser.zip Expand-Archive goreleaser.zip choco install make - name: Releasing diff --git a/.gitignore b/.gitignore index 42ffea73a..669a85871 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ pkg/visor/foo/ /*-server /*.json !/dmsghttp-config.json +!/services-config.json /*.sh /*.log diff --git a/.goreleaser-archlinux.yml b/.goreleaser-archlinux.yml deleted file mode 100644 index 34305b336..000000000 --- a/.goreleaser-archlinux.yml +++ /dev/null @@ -1,341 +0,0 @@ -# This is an example goreleaser.yaml file with some sane defaults. -# Make sure to check the documentation at http://goreleaser.com - -release: - # Repo in which the release will be created. - # Default is extracted from the origin remote URL or empty if its private hosted. - # Note: it can only be one: either github or gitlab or gitea - github: - owner: skycoin - name: skywire - - #prerelease: true - -before: - hooks: - - go mod tidy - - sed -i '/go conn.handleCall(msg)/c\conn.handleCall(msg)' ./vendor/github.com/godbus/dbus/v5/conn.go -builds: - - - id: skywire-visor-amd64 - binary: skywire-visor - goos: - - linux - goarch: - - amd64 - env: - - CGO_ENABLED=1 - - CC=musl-gcc - main: ./cmd/skywire-visor/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} -X github.com/skycoin/skywire/pkg/visor.BuildTag={{.Os}}_{{.Arch}} - - - id: skywire-visor-arm64 - binary: skywire-visor - goos: - - linux - goarch: - - arm64 - env: - - CGO_ENABLED=1 - - CC=aarch64-linux-musl-gcc - main: ./cmd/skywire-visor/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} -X github.com/skycoin/skywire/pkg/visor.BuildTag={{.Os}}_{{.Arch}} - - - id: skywire-visor-arm - binary: skywire-visor - goos: - - linux - goarch: - - arm - goarm: - - 6 - env: - - CGO_ENABLED=1 - - CC=arm-linux-gnueabihf-musl-gcc - main: ./cmd/skywire-visor/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} -X github.com/skycoin/skywire/pkg/visor.BuildTag={{.Os}}_{{.Arch}} - - - id: skywire-cli-amd64 - binary: skywire-cli - goos: - - linux - goarch: - - amd64 - env: - - CGO_ENABLED=1 - - CC=musl-gcc - main: ./cmd/skywire-cli/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} -X github.com/skycoin/skywire/pkg/visor.BuildTag={{.Os}}_{{.Arch}} - - - id: skywire-cli-arm64 - binary: skywire-cli - goos: - - linux - goarch: - - arm64 - env: - - CGO_ENABLED=1 - - CC=aarch64-linux-musl-gcc - main: ./cmd/skywire-cli/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} -X github.com/skycoin/skywire/pkg/visor.BuildTag={{.Os}}_{{.Arch}} - - - id: skywire-cli-arm - binary: skywire-cli - goos: - - linux - goarch: - - arm - goarm: - - 6 - env: - - CGO_ENABLED=1 - - CC=arm-linux-gnueabihf-musl-gcc - main: ./cmd/skywire-cli/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} -X github.com/skycoin/skywire/pkg/visor.BuildTag={{.Os}}_{{.Arch}} - - - id: skychat-amd64 - binary: apps/skychat - goos: - - linux - goarch: - - amd64 - env: - - CGO_ENABLED=1 - - CC=musl-gcc - main: ./cmd/apps/skychat/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - - - id: skychat-arm64 - binary: apps/skychat - goos: - - linux - goarch: - - arm64 - env: - - CGO_ENABLED=1 - - CC=aarch64-linux-musl-gcc - main: ./cmd/apps/skychat/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} -X github.com/skycoin/skywire/pkg/visor.BuildTag={{.Os}}_{{.Arch}} - - - id: skychat-arm - binary: apps/skychat - goos: - - linux - goarch: - - arm - goarm: - - 6 - env: - - CGO_ENABLED=1 - - CC=arm-linux-gnueabihf-musl-gcc - main: ./cmd/apps/skychat/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} -X github.com/skycoin/skywire/pkg/visor.BuildTag={{.Os}}_{{.Arch}} - - - id: skysocks-amd64 - binary: apps/skysocks - goos: - - linux - goarch: - - amd64 - env: - - CGO_ENABLED=1 - - CC=musl-gcc - main: ./cmd/apps/skysocks/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - - - id: skysocks-arm64 - binary: apps/skysocks - goos: - - linux - goarch: - - arm64 - env: - - CGO_ENABLED=1 - - CC=aarch64-linux-musl-gcc - main: ./cmd/apps/skysocks/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} -X github.com/skycoin/skywire/pkg/visor.BuildTag={{.Os}}_{{.Arch}} - - - id: skysocks-arm - binary: apps/skysocks - goos: - - linux - goarch: - - arm - goarm: - - 6 - env: - - CGO_ENABLED=1 - - CC=arm-linux-gnueabihf-musl-gcc - main: ./cmd/apps/skysocks/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} -X github.com/skycoin/skywire/pkg/visor.BuildTag={{.Os}}_{{.Arch}} - - - id: skysocks-client-amd64 - binary: apps/skysocks-client - goos: - - linux - goarch: - - amd64 - env: - - CGO_ENABLED=1 - - CC=musl-gcc - main: ./cmd/apps/skysocks-client/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - - - id: skysocks-client-arm64 - binary: apps/skysocks-client - goos: - - linux - goarch: - - arm64 - env: - - CGO_ENABLED=1 - - CC=aarch64-linux-musl-gcc - main: ./cmd/apps/skysocks-client/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} -X github.com/skycoin/skywire/pkg/visor.BuildTag={{.Os}}_{{.Arch}} - - - id: skysocks-client-arm - binary: apps/skysocks-client - goos: - - linux - goarch: - - arm - goarm: - - 6 - env: - - CGO_ENABLED=1 - - CC=arm-linux-gnueabihf-musl-gcc - main: ./cmd/apps/skysocks-client/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} -X github.com/skycoin/skywire/pkg/visor.BuildTag={{.Os}}_{{.Arch}} - - - id: vpn-server-amd64 - binary: apps/vpn-server - goos: - - linux - goarch: - - amd64 - env: - - CGO_ENABLED=1 - - CC=musl-gcc - main: ./cmd/apps/vpn-server/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - - - id: vpn-server-arm64 - binary: apps/vpn-server - goos: - - linux - goarch: - - arm64 - env: - - CGO_ENABLED=1 - - CC=aarch64-linux-musl-gcc - main: ./cmd/apps/vpn-server/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} -X github.com/skycoin/skywire/pkg/visor.BuildTag={{.Os}}_{{.Arch}} - - - id: vpn-server-arm - binary: apps/vpn-server - goos: - - linux - goarch: - - arm - goarm: - - 6 - env: - - CGO_ENABLED=1 - - CC=arm-linux-gnueabihf-musl-gcc - main: ./cmd/apps/vpn-server/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} -X github.com/skycoin/skywire/pkg/visor.BuildTag={{.Os}}_{{.Arch}} - - - id: vpn-client-amd64 - binary: apps/vpn-client - goos: - - linux - goarch: - - amd64 - env: - - CGO_ENABLED=1 - - CC=musl-gcc - main: ./cmd/apps/vpn-client/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - - - id: vpn-client-arm64 - binary: apps/vpn-client - goos: - - linux - goarch: - - arm64 - env: - - CGO_ENABLED=1 - - CC=aarch64-linux-musl-gcc - main: ./cmd/apps/vpn-client/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} -X github.com/skycoin/skywire/pkg/visor.BuildTag={{.Os}}_{{.Arch}} - - - id: vpn-client-arm - binary: apps/vpn-client - goos: - - linux - goarch: - - arm - goarm: - - 6 - env: - - CGO_ENABLED=1 - - CC=arm-linux-gnueabihf-musl-gcc - main: ./cmd/apps/vpn-client/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} -X github.com/skycoin/skywire/pkg/visor.BuildTag={{.Os}}_{{.Arch}} - -archives: - - id: amd64 - format: tar.gz - wrap_in_directory: false - name_template: 'skywire-v{{ .Version }}-{{ .Os }}-{{ .Arch }}' - files: - - dmsghttp-config.json - builds: - - skywire-visor-amd64 - - skywire-cli-amd64 - - skysocks-amd64 - - skysocks-client-amd64 - - skychat-amd64 - - vpn-server-amd64 - - vpn-client-amd64 - - - id: arm64 - format: tar.gz - wrap_in_directory: false - name_template: 'skywire-v{{ .Version }}-{{ .Os }}-{{ .Arch }}' - files: - - dmsghttp-config.json - builds: - - skywire-visor-arm64 - - skywire-cli-arm64 - - skysocks-arm64 - - skysocks-client-arm64 - - skychat-arm64 - - vpn-server-arm64 - - vpn-client-arm64 - - - id: arm - format: tar.gz - wrap_in_directory: false - name_template: 'skywire-v{{ .Version }}-{{ .Os }}-{{ .Arch }}' - files: - - dmsghttp-config.json - builds: - - skywire-visor-arm - - skywire-cli-arm - - skysocks-arm - - skysocks-client-arm - - skychat-arm - - vpn-server-arm - - vpn-client-arm - -checksum: - name_template: 'checksums.txt' -snapshot: - name_template: "{{ .Tag }}-next" -changelog: - sort: asc - filters: - exclude: - - '^docs:' - - '^test:' diff --git a/.goreleaser-darwin.yml b/.goreleaser-darwin.yml index 656f75af4..18b364819 100644 --- a/.goreleaser-darwin.yml +++ b/.goreleaser-darwin.yml @@ -15,8 +15,8 @@ before: hooks: - go mod tidy builds: - - id: skywire-visor - binary: skywire-visor + - id: skywire + binary: skywire goos: - darwin goarch: @@ -24,61 +24,9 @@ builds: - arm64 env: - CGO_ENABLED=1 - main: ./cmd/skywire-visor/ + main: ./cmd/skywire/ ldflags: -s -w -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} -X github.com/skycoin/skywire/pkg/visor.BuildTag={{.Os}} - - id: skywire-cli - binary: skywire-cli - goos: - - darwin - goarch: - - amd64 - - arm64 - env: - - CGO_ENABLED=1 - main: ./cmd/skywire-cli/ - ldflags: -s -w -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - - - id: skychat - binary: apps/skychat - goos: - - darwin - goarch: - - amd64 - - arm64 - main: ./cmd/apps/skychat/ - ldflags: -s -w -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - - - id: skysocks - binary: apps/skysocks - goos: - - darwin - goarch: - - amd64 - - arm64 - main: ./cmd/apps/skysocks/ - ldflags: -s -w -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - - - id: skysocks-client - binary: apps/skysocks-client - goos: - - darwin - goarch: - - amd64 - - arm64 - main: ./cmd/apps/skysocks-client/ - ldflags: -s -w -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - - - id: vpn-client - binary: apps/vpn-client - goos: - - darwin - goarch: - - amd64 - - arm64 - main: ./cmd/apps/vpn-client/ - ldflags: -s -w -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - archives: - id: archive format: tar.gz @@ -86,13 +34,9 @@ archives: name_template: 'skywire-v{{ .Version }}-{{ .Os }}-{{ .Arch }}' files: - dmsghttp-config.json + - services-config.json builds: - - skywire-visor - - skywire-cli - - skysocks - - skysocks-client - - skychat - - vpn-client + - skywire allow_different_binary_count: true checksum: diff --git a/.goreleaser-linux.yml b/.goreleaser-linux.yml index 1870d3eba..f4baf76ad 100644 --- a/.goreleaser-linux.yml +++ b/.goreleaser-linux.yml @@ -17,8 +17,8 @@ before: - sed -i '/go conn.handleCall(msg)/c\conn.handleCall(msg)' ./vendor/github.com/godbus/dbus/v5/conn.go builds: - - id: skywire-visor-amd64 - binary: skywire-visor + - id: skywire-amd64 + binary: skywire goos: - linux goarch: @@ -26,11 +26,11 @@ builds: env: - CGO_ENABLED=1 - CC=/home/runner/work/skywire/skywire/musl-data/x86_64-linux-musl-cross/bin/x86_64-linux-musl-gcc - main: ./cmd/skywire-visor/ + main: ./cmd/skywire/ ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} -X github.com/skycoin/skywire/pkg/visor.BuildTag={{.Os}}_{{.Arch}} - - id: skywire-visor-arm64 - binary: skywire-visor + - id: skywire-arm64 + binary: skywire goos: - linux goarch: @@ -38,11 +38,11 @@ builds: env: - CGO_ENABLED=1 - CC=/home/runner/work/skywire/skywire/musl-data/aarch64-linux-musl-cross/bin/aarch64-linux-musl-gcc - main: ./cmd/skywire-visor/ + main: ./cmd/skywire/ ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} -X github.com/skycoin/skywire/pkg/visor.BuildTag={{.Os}}_{{.Arch}} - - id: skywire-visor-arm - binary: skywire-visor + - id: skywire-arm + binary: skywire goos: - linux goarch: @@ -52,11 +52,11 @@ builds: env: - CGO_ENABLED=1 - CC=/home/runner/work/skywire/skywire/musl-data/arm-linux-musleabi-cross/bin/arm-linux-musleabi-gcc - main: ./cmd/skywire-visor/ + main: ./cmd/skywire/ ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} -X github.com/skycoin/skywire/pkg/visor.BuildTag={{.Os}}_{{.Arch}} - - id: skywire-visor-armhf - binary: skywire-visor + - id: skywire-armhf + binary: skywire goos: - linux goarch: @@ -66,11 +66,11 @@ builds: env: - CGO_ENABLED=1 - CC=/home/runner/work/skywire/skywire/musl-data/arm-linux-musleabihf-cross/bin/arm-linux-musleabihf-gcc - main: ./cmd/skywire-visor/ + main: ./cmd/skywire/ ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} -X github.com/skycoin/skywire/pkg/visor.BuildTag={{.Os}}_{{.Arch}} - - id: skywire-visor-riscv64 - binary: skywire-visor + - id: skywire-riscv64 + binary: skywire goos: - linux goarch: @@ -78,393 +78,9 @@ builds: env: - CGO_ENABLED=1 - CC=/home/runner/work/skywire/skywire/musl-data/riscv64-linux-musl-cross/bin/riscv64-linux-musl-gcc - main: ./cmd/skywire-visor/ + main: ./cmd/skywire/ ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} -X github.com/skycoin/skywire/pkg/visor.BuildTag={{.Os}}_{{.Arch}} - - id: skywire-cli-amd64 - binary: skywire-cli - goos: - - linux - goarch: - - amd64 - env: - - CGO_ENABLED=1 - - CC=/home/runner/work/skywire/skywire/musl-data/x86_64-linux-musl-cross/bin/x86_64-linux-musl-gcc - main: ./cmd/skywire-cli/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} -X github.com/skycoin/skywire/pkg/visor.BuildTag={{.Os}}_{{.Arch}} - - - id: skywire-cli-arm64 - binary: skywire-cli - goos: - - linux - goarch: - - arm64 - env: - - CGO_ENABLED=1 - - CC=/home/runner/work/skywire/skywire/musl-data/aarch64-linux-musl-cross/bin/aarch64-linux-musl-gcc - main: ./cmd/skywire-cli/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} -X github.com/skycoin/skywire/pkg/visor.BuildTag={{.Os}}_{{.Arch}} - - - id: skywire-cli-arm - binary: skywire-cli - goos: - - linux - goarch: - - arm - goarm: - - 6 - env: - - CGO_ENABLED=1 - - CC=/home/runner/work/skywire/skywire/musl-data/arm-linux-musleabi-cross/bin/arm-linux-musleabi-gcc - main: ./cmd/skywire-cli/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} -X github.com/skycoin/skywire/pkg/visor.BuildTag={{.Os}}_{{.Arch}} - - - id: skywire-cli-armhf - binary: skywire-cli - goos: - - linux - goarch: - - arm - goarm: - - 7 - env: - - CGO_ENABLED=1 - - CC=/home/runner/work/skywire/skywire/musl-data/arm-linux-musleabihf-cross/bin/arm-linux-musleabihf-gcc - main: ./cmd/skywire-cli/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} -X github.com/skycoin/skywire/pkg/visor.BuildTag={{.Os}}_{{.Arch}} - - - id: skywire-cli-riscv64 - binary: skywire-cli - goos: - - linux - goarch: - - riscv64 - env: - - CGO_ENABLED=1 - - CC=/home/runner/work/skywire/skywire/musl-data/riscv64-linux-musl-cross/bin/riscv64-linux-musl-gcc - main: ./cmd/skywire-cli/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} -X github.com/skycoin/skywire/pkg/visor.BuildTag={{.Os}}_{{.Arch}} - - - id: skychat-amd64 - binary: apps/skychat - goos: - - linux - goarch: - - amd64 - env: - - CGO_ENABLED=1 - - CC=/home/runner/work/skywire/skywire/musl-data/x86_64-linux-musl-cross/bin/x86_64-linux-musl-gcc - main: ./cmd/apps/skychat/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - - - id: skychat-arm64 - binary: apps/skychat - goos: - - linux - goarch: - - arm64 - env: - - CGO_ENABLED=1 - - CC=/home/runner/work/skywire/skywire/musl-data/aarch64-linux-musl-cross/bin/aarch64-linux-musl-gcc - main: ./cmd/apps/skychat/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - - - id: skychat-arm - binary: apps/skychat - goos: - - linux - goarch: - - arm - goarm: - - 6 - env: - - CGO_ENABLED=1 - - CC=/home/runner/work/skywire/skywire/musl-data/arm-linux-musleabi-cross/bin/arm-linux-musleabi-gcc - main: ./cmd/apps/skychat/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - - - id: skychat-armhf - binary: apps/skychat - goos: - - linux - goarch: - - arm - goarm: - - 7 - env: - - CGO_ENABLED=1 - - CC=/home/runner/work/skywire/skywire/musl-data/arm-linux-musleabihf-cross/bin/arm-linux-musleabihf-gcc - main: ./cmd/apps/skychat/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - - - id: skychat-riscv64 - binary: apps/skychat - goos: - - linux - goarch: - - riscv64 - env: - - CGO_ENABLED=1 - - CC=/home/runner/work/skywire/skywire/musl-data/riscv64-linux-musl-cross/bin/riscv64-linux-musl-gcc - main: ./cmd/apps/skychat/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - - - id: skysocks-amd64 - binary: apps/skysocks - goos: - - linux - goarch: - - amd64 - env: - - CGO_ENABLED=1 - - CC=/home/runner/work/skywire/skywire/musl-data/x86_64-linux-musl-cross/bin/x86_64-linux-musl-gcc - main: ./cmd/apps/skysocks/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - - - id: skysocks-arm64 - binary: apps/skysocks - goos: - - linux - goarch: - - arm64 - env: - - CGO_ENABLED=1 - - CC=/home/runner/work/skywire/skywire/musl-data/aarch64-linux-musl-cross/bin/aarch64-linux-musl-gcc - main: ./cmd/apps/skysocks/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - - - id: skysocks-arm - binary: apps/skysocks - goos: - - linux - goarch: - - arm - goarm: - - 6 - env: - - CGO_ENABLED=1 - - CC=/home/runner/work/skywire/skywire/musl-data/arm-linux-musleabi-cross/bin/arm-linux-musleabi-gcc - main: ./cmd/apps/skysocks/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - - - id: skysocks-armhf - binary: apps/skysocks - goos: - - linux - goarch: - - arm - goarm: - - 7 - env: - - CGO_ENABLED=1 - - CC=/home/runner/work/skywire/skywire/musl-data/arm-linux-musleabihf-cross/bin/arm-linux-musleabihf-gcc - main: ./cmd/apps/skysocks/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - - - id: skysocks-riscv64 - binary: apps/skysocks - goos: - - linux - goarch: - - riscv64 - env: - - CGO_ENABLED=1 - - CC=/home/runner/work/skywire/skywire/musl-data/riscv64-linux-musl-cross/bin/riscv64-linux-musl-gcc - main: ./cmd/apps/skysocks/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - - - id: skysocks-client-amd64 - binary: apps/skysocks-client - goos: - - linux - goarch: - - amd64 - env: - - CGO_ENABLED=1 - - CC=/home/runner/work/skywire/skywire/musl-data/x86_64-linux-musl-cross/bin/x86_64-linux-musl-gcc - main: ./cmd/apps/skysocks-client/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - - - id: skysocks-client-arm64 - binary: apps/skysocks-client - goos: - - linux - goarch: - - arm64 - env: - - CGO_ENABLED=1 - - CC=/home/runner/work/skywire/skywire/musl-data/aarch64-linux-musl-cross/bin/aarch64-linux-musl-gcc - main: ./cmd/apps/skysocks-client/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - - - id: skysocks-client-arm - binary: apps/skysocks-client - goos: - - linux - goarch: - - arm - goarm: - - 6 - env: - - CGO_ENABLED=1 - - CC=/home/runner/work/skywire/skywire/musl-data/arm-linux-musleabi-cross/bin/arm-linux-musleabi-gcc - main: ./cmd/apps/skysocks-client/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - - - id: skysocks-client-armhf - binary: apps/skysocks-client - goos: - - linux - goarch: - - arm - goarm: - - 7 - env: - - CGO_ENABLED=1 - - CC=/home/runner/work/skywire/skywire/musl-data/arm-linux-musleabihf-cross/bin/arm-linux-musleabihf-gcc - main: ./cmd/apps/skysocks-client/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - - - id: skysocks-client-riscv64 - binary: apps/skysocks-client - goos: - - linux - goarch: - - riscv64 - env: - - CGO_ENABLED=1 - - CC=/home/runner/work/skywire/skywire/musl-data/riscv64-linux-musl-cross/bin/riscv64-linux-musl-gcc - main: ./cmd/apps/skysocks-client/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - - - id: vpn-server-amd64 - binary: apps/vpn-server - goos: - - linux - goarch: - - amd64 - env: - - CGO_ENABLED=1 - - CC=/home/runner/work/skywire/skywire/musl-data/x86_64-linux-musl-cross/bin/x86_64-linux-musl-gcc - main: ./cmd/apps/vpn-server/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - - - id: vpn-server-arm64 - binary: apps/vpn-server - goos: - - linux - goarch: - - arm64 - env: - - CGO_ENABLED=1 - - CC=/home/runner/work/skywire/skywire/musl-data/aarch64-linux-musl-cross/bin/aarch64-linux-musl-gcc - main: ./cmd/apps/vpn-server/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - - - id: vpn-server-arm - binary: apps/vpn-server - goos: - - linux - goarch: - - arm - goarm: - - 6 - env: - - CGO_ENABLED=1 - - CC=/home/runner/work/skywire/skywire/musl-data/arm-linux-musleabi-cross/bin/arm-linux-musleabi-gcc - main: ./cmd/apps/vpn-server/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - - - id: vpn-server-armhf - binary: apps/vpn-server - goos: - - linux - goarch: - - arm - goarm: - - 7 - env: - - CGO_ENABLED=1 - - CC=/home/runner/work/skywire/skywire/musl-data/arm-linux-musleabihf-cross/bin/arm-linux-musleabihf-gcc - main: ./cmd/apps/vpn-server/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - - - id: vpn-server-riscv64 - binary: apps/vpn-server - goos: - - linux - goarch: - - riscv64 - env: - - CGO_ENABLED=1 - - CC=/home/runner/work/skywire/skywire/musl-data/riscv64-linux-musl-cross/bin/riscv64-linux-musl-gcc - main: ./cmd/apps/vpn-server/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - - - id: vpn-client-amd64 - binary: apps/vpn-client - goos: - - linux - goarch: - - amd64 - env: - - CGO_ENABLED=1 - - CC=/home/runner/work/skywire/skywire/musl-data/x86_64-linux-musl-cross/bin/x86_64-linux-musl-gcc - main: ./cmd/apps/vpn-client/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - - - id: vpn-client-arm64 - binary: apps/vpn-client - goos: - - linux - goarch: - - arm64 - env: - - CGO_ENABLED=1 - - CC=/home/runner/work/skywire/skywire/musl-data/aarch64-linux-musl-cross/bin/aarch64-linux-musl-gcc - main: ./cmd/apps/vpn-client/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - - - id: vpn-client-arm - binary: apps/vpn-client - goos: - - linux - goarch: - - arm - goarm: - - 6 - env: - - CGO_ENABLED=1 - - CC=/home/runner/work/skywire/skywire/musl-data/arm-linux-musleabi-cross/bin/arm-linux-musleabi-gcc - main: ./cmd/apps/vpn-client/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - - - id: vpn-client-armhf - binary: apps/vpn-client - goos: - - linux - goarch: - - arm - goarm: - - 7 - env: - - CGO_ENABLED=1 - - CC=/home/runner/work/skywire/skywire/musl-data/arm-linux-musleabihf-cross/bin/arm-linux-musleabihf-gcc - main: ./cmd/apps/vpn-client/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - - - id: vpn-client-riscv64 - binary: apps/vpn-client - goos: - - linux - goarch: - - riscv64 - env: - - CGO_ENABLED=1 - - CC=/home/runner/work/skywire/skywire/musl-data/riscv64-linux-musl-cross/bin/riscv64-linux-musl-gcc - main: ./cmd/apps/vpn-client/ - ldflags: -s -w -linkmode external -extldflags '-static' -buildid= -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - archives: - id: amd64 format: tar.gz @@ -472,14 +88,9 @@ archives: name_template: 'skywire-v{{ .Version }}-{{ .Os }}-{{ .Arch }}' files: - dmsghttp-config.json + - services-config.json builds: - - skywire-visor-amd64 - - skywire-cli-amd64 - - skysocks-amd64 - - skysocks-client-amd64 - - skychat-amd64 - - vpn-server-amd64 - - vpn-client-amd64 + - skywire-amd64 - id: arm64 format: tar.gz @@ -487,14 +98,9 @@ archives: name_template: 'skywire-v{{ .Version }}-{{ .Os }}-{{ .Arch }}' files: - dmsghttp-config.json + - services-config.json builds: - - skywire-visor-arm64 - - skywire-cli-arm64 - - skysocks-arm64 - - skysocks-client-arm64 - - skychat-arm64 - - vpn-server-arm64 - - vpn-client-arm64 + - skywire-arm64 - id: arm format: tar.gz @@ -502,14 +108,9 @@ archives: name_template: 'skywire-v{{ .Version }}-{{ .Os }}-{{ .Arch }}' files: - dmsghttp-config.json + - services-config.json builds: - - skywire-visor-arm - - skywire-cli-arm - - skysocks-arm - - skysocks-client-arm - - skychat-arm - - vpn-server-arm - - vpn-client-arm + - skywire-arm - id: armhf format: tar.gz @@ -517,14 +118,9 @@ archives: name_template: 'skywire-v{{ .Version }}-{{ .Os }}-{{ .Arch }}hf' files: - dmsghttp-config.json + - services-config.json builds: - - skywire-visor-armhf - - skywire-cli-armhf - - skysocks-armhf - - skysocks-client-armhf - - skychat-armhf - - vpn-server-armhf - - vpn-client-armhf + - skywire-armhf - id: riscv64 format: tar.gz @@ -532,14 +128,9 @@ archives: name_template: 'skywire-v{{ .Version }}-{{ .Os }}-{{ .Arch }}' files: - dmsghttp-config.json + - services-config.json builds: - - skywire-visor-riscv64 - - skywire-cli-riscv64 - - skysocks-riscv64 - - skysocks-client-riscv64 - - skychat-riscv64 - - vpn-server-riscv64 - - vpn-client-riscv64 + - skywire-riscv64 checksum: name_template: 'checksums.txt' diff --git a/.goreleaser-windows.yml b/.goreleaser-windows.yml index c6f867790..da03241b5 100644 --- a/.goreleaser-windows.yml +++ b/.goreleaser-windows.yml @@ -15,8 +15,8 @@ before: hooks: - go mod tidy builds: - - id: skywire-visor - binary: skywire-visor + - id: skywire + binary: skywire goos: - windows goarch: @@ -24,44 +24,9 @@ builds: - 386 env: - CGO_ENABLED=1 - main: ./cmd/skywire-visor/ + main: ./cmd/skywire/ ldflags: -s -w -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} -X github.com/skycoin/skywire/pkg/visor.BuildTag={{.Os}} - - id: skywire-cli - binary: skywire-cli - goos: - - windows - goarch: - - amd64 - - 386 - env: - - CGO_ENABLED=0 - main: ./cmd/skywire-cli/ - ldflags: -s -w -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - - - id: vpn-client - binary: apps/vpn-client - goos: - - windows - goarch: - - amd64 - - 386 - env: - - CGO_ENABLED=0 - main: ./cmd/apps/vpn-client/ - ldflags: -s -w -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} - - - id: skysocks-client - binary: apps/skysocks-client - goos: - - windows - goarch: - - amd64 - - 386 - env: - - CGO_ENABLED=0 - main: ./cmd/apps/skysocks-client/ - ldflags: -s -w -X github.com/skycoin/skywire-utilities/pkg/buildinfo.version=v{{.Version}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.commit={{.ShortCommit}} -X github.com/skycoin/skywire-utilities/pkg/buildinfo.date={{.Date}} archives: - id: archive @@ -70,11 +35,9 @@ archives: name_template: 'skywire-v{{ .Version }}-{{ .Os }}-{{ .Arch }}' files: - dmsghttp-config.json + - services-config.json builds: - - skywire-visor - - skywire-cli - - vpn-client - - skysocks-client + - skywire allow_different_binary_count: true checksum: diff --git a/CHANGELOG.md b/CHANGELOG.md index 600d66f3d..2b8d2aea0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. updates may be generated with `scripts/changelog.sh ` +## 1.3.17 +- Add http-proxy on skysocks-client [#1728](https://github.com/skycoin/skywire/pull/1728) +- Little Improve on skywire and setup-node [#1723](https://github.com/skycoin/skywire/pull/1723) +- Improve VPN and Proxy cli command [#1722](https://github.com/skycoin/skywire/pull/1722) +- Improve Survey and Log Collection [#1721](https://github.com/skycoin/skywire/pull/1721) +- Server list optimization [#1720](https://github.com/skycoin/skywire/pull/1720) +- Fix reward calc [#1719](https://github.com/skycoin/skywire/pull/1719) +- Fix reward calculation [#1716](https://github.com/skycoin/skywire/pull/1716) +- Fix log collection panic [#1711](https://github.com/skycoin/skywire/pull/1711) +- Fix win installer script [#1706](https://github.com/skycoin/skywire/pull/1706) + ## 1.3.16 - fix VPN issues on CI and Windows [#1703](https://github.com/skycoin/skywire/pull/1703) diff --git a/Makefile b/Makefile index ace5669e0..4ce0cb181 100644 --- a/Makefile +++ b/Makefile @@ -97,30 +97,46 @@ date: commit: @echo $(COMMIT) -check: lint test ## Run linters and tests +check: lint check-cg test ## Run linters and tests + +check-cg: ## Cursory check of the main help menu, offline dmsghttp config gen and offline config gen + @echo "checking help menu for compilation without errors" + @echo + go run cmd/skywire/skywire.go --help + @echo + @echo "checking dmsghttp offline config gen" + @echo + go run cmd/skywire/skywire.go cli config gen --nofetch -dnw + @echo + @echo "checking offline config gen" + @echo + go run cmd/skywire/skywire.go cli config gen --nofetch -nw + @echo + @echo "config gen succeeded without error" + @echo -check-windows: lint-windows test-windows ## Run linters and tests on windows image - -build: host-apps bin ## Install dependencies, build apps and binaries. `go build` with ${OPTS} -build-windows: host-apps-windows bin-windows ## Install dependencies, build apps and binaries. `go build` with ${OPTS} +check-windows: lint-windows test-windows ## Run linters and tests on windows image -build-static: host-apps-static bin-static ## Build apps and binaries. `go build` with ${OPTS} +build: clean build-merged ## Install dependencies, build apps and binaries. `go build` with ${OPTS} -build-static-wos: host-apps-static bin-static-wos ## Build apps and binaries. `go build` with ${OPTS} +build-merged: ## Install dependencies, build apps and binaries. `go build` with ${OPTS} + ${OPTS} go build ${BUILD_OPTS} -o $(BUILD_PATH)skywire ./cmd/skywire -build-example: host-apps example-apps bin ## Build apps, example apps and binaries. `go build` with ${OPTS} - -installer: mac-installer ## Builds MacOS installer for skywire-visor +build-merged-windows: clean-windows + powershell '${OPTS} go build ${BUILD_OPTS} -o $(BUILD_PATH)skywire.exe ./cmd/skywire' install-system-linux: build ## Install apps and binaries over those provided by the linux package - linux package must be installed first! sudo echo "sudo cache" - sudo install -Dm755 $(BUILD_PATH){skywire-cli,skywire-visor} /opt/skywire/bin/ & \ - sudo install -Dm755 $(BUILD_PATH)apps/{vpn-server,vpn-client,skysocks-client,skysocks,skychat} /opt/skywire/apps/ + sudo install -Dm755 $(BUILD_PATH)skywire /opt/skywire/bin/ + install-generate: ## Installs required execs for go generate. ${OPTS} go install github.com/mjibson/esc github.com/vektra/mockery/v2@latest + ## TO DO: it may be unnecessary to install required execs for go generate into the path. An alternative method may exist which does not require this + ## https://eli.thegreenplace.net/2021/a-comprehensive-guide-to-go-generate + generate: ## Generate mocks and config README's go generate ./... @@ -132,13 +148,13 @@ clean-windows: ## Clean project: remove created binaries and apps powershell -Command "If (Test-Path ./build) { Remove-Item -Path ./build -Force -Recurse }" install: ## Install `skywire-visor`, `skywire-cli`, `setup-node` - ${OPTS} go install ${BUILD_OPTS} ./cmd/skywire-visor ./cmd/skywire-cli ./cmd/setup-node + ${OPTS} go install ${BUILD_OPTS} ./cmd/skywire install-windows: ## Install `skywire-visor`, `skywire-cli`, `setup-node` powershell 'Get-ChildItem .\cmd | % { ${OPTS} go install ${BUILD_OPTS} ./ $$_.FullName }' install-static: ## Install `skywire-visor`, `skywire-cli`, `setup-node` - ${STATIC_OPTS} go install -trimpath --ldflags '-linkmode external -extldflags "-static" -buildid=' ./cmd/skywire-visor ./cmd/skywire-cli ./cmd/setup-node + ${STATIC_OPTS} go install -trimpath --ldflags '-linkmode external -extldflags "-static" -buildid=' ./cmd/skywire lint: ## Run linters. Use make install-linters first golangci-lint --version @@ -152,10 +168,13 @@ test: ## Run tests -go clean -testcache &>/dev/null ${OPTS} go test ${TEST_OPTS} ./internal/... ./pkg/... ./cmd/... ${OPTS} go test ${TEST_OPTS} + go run cmd/skywire/skywire.go --help + go run cmd/skywire/skywire.go cli config gen -dnw + go run cmd/skywire/skywire.go cli config gen --nofetch -nw test-windows: ## Run tests on windows @go clean -testcache - ${OPTS} go test ${TEST_OPTS} ./internal/... ./pkg/... ./cmd/... + ${OPTS} go test ${TEST_OPTS} ./internal/... ./pkg/... ./cmd/skywire-cli... ./cmd/skywire-visor... ./cmd/skywire... ./cmd/apps... install-linters: ## Install linters - VERSION=latest ./ci_scripts/install-golangci-lint.sh @@ -186,39 +205,14 @@ snapshot-linux: ## goreleaser --snapshot --config .goreleaser-linux.yml --skip- snapshot-clean: ## Cleans snapshot / release rm -rf ./dist -host-apps: ## Build app - test -d $(BUILD_PATH) && rm -r $(BUILD_PATH) || true - mkdir -p $(BUILD_PATH)apps - ${OPTS} go build ${BUILD_OPTS} -o $(BUILD_PATH)apps/ ./cmd/apps/... - example-apps: ## Build example apps ${OPTS} go build ${BUILD_OPTS} -o $(BUILD_PATH)apps/ ./example/... -host-apps-windows: ## build apps on windows - powershell -Command new-item $(BUILD_PATH)apps -itemtype directory -force - powershell 'Get-ChildItem .\cmd\apps | % { ${OPTS} go build ${BUILD_OPTS} -o $(BUILD_PATH)apps $$_.FullName }' - -# Static Apps -host-apps-static: ## Build app - test -d apps && rm -r apps || true - mkdir -p ./apps - ${STATIC_OPTS} go build -trimpath --ldflags '-linkmode external -extldflags "-static" -buildid=' -o $(BUILD_PATH)apps/ ./cmd/apps/... - -host-apps-deploy: ## Build app - test -d apps && rm -r apps || true - mkdir -p ./apps - ${OPTS} go build ${BUILD_OPTS_DEPLOY} -o $(BUILD_PATH)apps/ ./cmd/apps/... - -host-apps-race: ## Build app - test -d apps && rm -r apps || true - mkdir -p ./apps - CGO_ENABLED=1${OPTS} go build ${BUILD_OPTS} -race -o $(BUILD_PATH)apps/ ./cmd/apps/... - # Bin bin: fix-systray-vendor bin-fix unfix-systray-vendor -bin-fix: ## Build `skywire-visor`, `skywire-cli` - ${OPTS} go build ${BUILD_OPTS} -o $(BUILD_PATH) ./cmd/skywire-visor ./cmd/skywire-cli ./cmd/setup-node +bin-fix: ## Build `skywire` + ${OPTS} go build ${BUILD_OPTS} -o $(BUILD_PATH) ./cmd/skywire fix-systray-vendor: @if [ $(UNAME_S) = "Linux" ]; then\ @@ -230,25 +224,22 @@ unfix-systray-vendor: sed -i '/conn.handleCall(msg)/c\ go conn.handleCall(msg)' ./vendor/github.com/godbus/dbus/v5/conn.go ;\ fi -bin-windows: ## Build `skywire-visor`, `skywire-cli` - powershell 'Get-ChildItem .\cmd | % { ${OPTS} go build ${BUILD_OPTS} -o $(BUILD_PATH) $$_.FullName }' +build-windows: ## Build `skywire-visor` + powershell '${OPTS} go build ${BUILD_OPTS} -o $(BUILD_PATH) ./cmd/skywire' # Static Bin -bin-static: ## Build `skywire-visor`, `skywire-cli` - ${STATIC_OPTS} go build 8 -trimpath --ldflags '-linkmode external -extldflags "-static" -buildid=' -o $(BUILD_PATH) ./cmd/skywire-visor ./cmd/skywire-cli ./cmd/setup-node +build-static: ## Build `skywire-visor`, `skywire-cli` + ${STATIC_OPTS} go build 8 -trimpath --ldflags '-linkmode external -extldflags "-static" -buildid=' -o $(BUILD_PATH) ./cmd/skywire # Static Bin without Systray -bin-static-wos: ## Build `skywire-visor`, `skywire-cli` - ${STATIC_OPTS} go build -tags withoutsystray -trimpath --ldflags '-linkmode external -extldflags "-static" -buildid=' -o $(BUILD_PATH)skywire-visor ./cmd/skywire-visor - ${STATIC_OPTS} go build -trimpath --ldflags '-linkmode external -extldflags "-static" -buildid=' -o $(BUILD_PATH) ./cmd/skywire-cli ./cmd/setup-node +build-static-wos: ## Build `skywire-visor`, `skywire-cli` + ${STATIC_OPTS} go build -tags withoutsystray -trimpath --ldflags '-linkmode external -extldflags "-static" -buildid=' -o $(BUILD_PATH)skywire-visor ./cmd/skywire -build-deploy: host-apps-deploy ## Build for deployment Docker images - ${OPTS} go build -tags netgo ${BUILD_OPTS_DEPLOY} -o /release/skywire-visor ./cmd/skywire-visor - ${OPTS} go build ${BUILD_OPTS_DEPLOY} -o /release/skywire-cli ./cmd/skywire-cli +build-deploy: ## Build for deployment Docker images + ${OPTS} go build -tags netgo ${BUILD_OPTS_DEPLOY} -o /release/skywire ./cmd/skywire -build-race: host-apps-race ## Build for testing Docker images - CGO_ENABLED=1 ${OPTS} go build -tags netgo ${BUILD_OPTS} -race -o /release/skywire-visor ./cmd/skywire-visor - CGO_ENABLED=1 ${OPTS} go build ${BUILD_OPTS} -race -o /release/skywire-cli ./cmd/skywire-cli +build-race: ## Build for testing Docker images + CGO_ENABLED=1 ${OPTS} go build -tags netgo ${BUILD_OPTS} -race -o /release/skywire ./cmd/skywire github-prepare-release: $(eval GITHUB_TAG=$(shell git describe --abbrev=0 --tags | cut -c 2-6)) @@ -257,9 +248,6 @@ github-prepare-release: github-release: github-prepare-release goreleaser --rm-dist --config .goreleaser-linux.yml --release-notes releaseChangelog.md -github-release-archlinux: github-prepare-release - goreleaser --rm-dist --config .goreleaser-archlinux.yml --release-notes releaseChangelog.md - github-release-darwin: goreleaser --rm-dist --config .goreleaser-darwin.yml --skip-publish $(eval GITHUB_TAG=$(shell git describe --abbrev=0 --tags)) @@ -307,44 +295,27 @@ run: ## Run skywire visor with skywire-config.json, and start a browser if runni ## Prepare to run skywire from source, without compiling binaries prepare: test -d apps && rm -r apps || true - mkdir -p apps - ln ./scripts/_apps/skychat ./apps/ - ln ./scripts/_apps/skysocks ./apps/ - ln ./scripts/_apps/skysocks-client ./apps/ - ln ./scripts/_apps/vpn-server ./apps/ - ln ./scripts/_apps/vpn-client ./apps/ - chmod +x ./apps/* + test -d build && rm -r build || true + mkdir -p build || true + ln ./scripts/skywire ./build/ + chmod +x ./build/* sudo echo "sudo cache" + run-source: prepare ## Run skywire from source, without compiling binaries - go run ./cmd/skywire-cli/skywire-cli.go config gen -in | sudo go run ./cmd/skywire-visor/skywire-visor.go -n || true + go run ./cmd/skywire/skywire.go cli config gen -in | sudo go run ./cmd/skywire/skywire.go visor -n || true run-systray: prepare ## Run skywire from source, with vpn server enabled - go run ./cmd/skywire-cli/skywire-cli.go config gen -ni | sudo go run ./cmd/skywire-visor/skywire-visor.go -n --systray || true + go run ./cmd/skywire/skywire.go cli config gen -ni | sudo go run ./cmd/skywire/skywire.go visor -n --systray || true run-vpnsrv: prepare ## Run skywire from source, without compiling binaries - go run ./cmd/skywire-cli/skywire-cli.go config gen -in --servevpn | sudo go run ./cmd/skywire-visor/skywire-visor.go -n || true - -run-source-test: prepare ## Run skywire from source with test endpoints - go run ./cmd/skywire-cli/skywire-cli.go config gen -nit | sudo go run ./cmd/skywire-visor/skywire-visor.go -n || true - -run-vpnsrv-test: prepare ## Run skywire from source, with vpn server enabled - go run ./cmd/skywire-cli/skywire-cli.go config gen -nit --servevpn | sudo go run ./cmd/skywire-visor/skywire-visor.go -n || true - -run-systray-test: prepare ## Run skywire from source, with vpn server enabled - go run ./cmd/skywire-cli/skywire-cli.go config gen -nit | sudo go run ./cmd/skywire-visor/skywire-visor.go --systray -n || true + go run ./cmd/skywire/skywire.go cli config gen -in --servevpn | sudo go run ./cmd/skywire/skywire.go visor -n || true run-source-dmsghttp: prepare ## Run skywire from source with dmsghttp config - go run ./cmd/skywire-cli/skywire-cli.go config gen -din | sudo go run ./cmd/skywire-visor/skywire-visor.go -n || true + go run ./cmd/skywire/skywire.go cli config gen -din | sudo go run ./cmd/skywire/skywire.go visor -n || true run-vpnsrv-dmsghttp: prepare ## Run skywire from source with dmsghttp config and vpn server - go run ./cmd/skywire-cli/skywire-cli.go config gen -din --servevpn | sudo go run ./cmd/skywire-visor/skywire-visor.go -n || true - -run-source-dmsghttp-test: prepare ## Run skywire from source with dmsghttp config and test endpoints - go run ./cmd/skywire-cli/skywire-cli.go config gen -dint | sudo go run ./cmd/skywire-visor/skywire-visor.go -n || true - -run-vpnsrv-dmsghttp-test: prepare ## Run skywire from source with dmsghttp config, vpn server, and test endpoints - go run ./cmd/skywire-cli/skywire-cli.go config gen -dint --servevpn | sudo go run ./cmd/skywire-visor/skywire-visor.go -n || true + go run ./cmd/skywire/skywire.go cli config gen -din --servevpn | sudo go run ./cmd/skywire/skywire.go visor -n || true lint-ui: ## Lint the UI code cd $(MANAGER_UI_DIR) && npm run lint @@ -362,6 +333,8 @@ build-ui-windows: install-deps-ui ## Builds the UI on windows powershell 'New-Item -Path ${MANAGER_UI_BUILT_DIR} -ItemType Directory' powershell 'Copy-Item -Recurse ${MANAGER_UI_DIR}\dist\* ${MANAGER_UI_BUILT_DIR}' +installer: mac-installer ## Builds MacOS installer for skywire-visor + mac-installer: ## Create unsigned and not-notarized application, run make mac-installer-help for more ./scripts/mac_installer/create_installer.sh diff --git a/README.md b/README.md index d88cf5f11..bde281b94 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/skycoin/skywire)](https://goreportcard.com/report/github.com/skycoin/skywire) -[![Test](https://github.com/skycoin/skywire/actions/workflows/test.yml/badge.svg) -[![Deploy](https://github.com/skycoin/skywire/actions/workflows/deploy.yml/badge.svg) -[![Release](https://github.com/skycoin/skywire/actions/workflows/release.yml/badge.svg) +![Test](https://github.com/skycoin/skywire/actions/workflows/test.yml/badge.svg) +![Deploy](https://github.com/skycoin/skywire/actions/workflows/deploy.yml/badge.svg) +![Release](https://github.com/skycoin/skywire/actions/workflows/release.yml/badge.svg) [![GitHub release](https://img.shields.io/github/release/skycoin/skywire.svg)](https://github.com/skycoin/skywire/releases/) [![skywire](https://img.shields.io/aur/version/skywire?color=1793d1&label=skywire&logo=arch-linux)](https://aur.archlinux.org/packages/skywire/) [![skywire-bin](https://img.shields.io/aur/version/skywire-bin?color=1793d1&label=skywire-bin&logo=arch-linux)](https://aur.archlinux.org/packages/skywire-bin/) @@ -42,14 +42,15 @@ Compiling Skywire requires a Golang version of at least `1.16`. ## Commands and Subcommands -Documentation on skywire-cli interface as well as available flags for skywire-visor: +Documentation of all commands and subcommands is available in the command documentation README: +* [skywire](cmd/skywire/README.md) * [skywire-cli](cmd/skywire-cli/README.md) * [skywire-visor](cmd/skywire-visor/README.md) -## App documentation +## Visor Native Applications -Apps are not executed by the user, but hosted by the visor process. +Visor apps are not executed directly by the user, but hosted by the visor process. * [API](docs/skywire_app_api.md) * [skychat](cmd/apps/skychat/README.md) @@ -62,15 +63,13 @@ Apps are not executed by the user, but hosted by the visor process. Further documentation can be found in the [skywire wiki](https://github.com/skycoin/skywire/wiki). -## Installing Skywire +## Installing Skywire from Release -Pre-compiled resouces: +Releases for windows & macOS are available from the [release section](https://github.com/skycoin/skywire/releases/) -* [Windows installer](https://github.com/skycoin/skywire/releases/download/v1.3.7/skywire-installer-v1.3.7-windows-amd64.msi) -* [MacOS amd64 package](https://github.com/skycoin/skywire/releases/download/v1.3.7/skywire-installer-v1.3.7-darwin-amd64.pkg) -* [MacOS m1 / arm64](https://github.com/skycoin/skywire/releases/download/v1.3.7/skywire-installer-v1.3.7-darwin-arm64.pkg) -* [Debian Package Installation Guide](https://github.com/skycoin/skywire/wiki/Skywire-Package-Installation) -* [Binary Releases](https://github.com/skycoin/skywire/releases) +Install as a package on debian or arch linux: [Package Installation Guide](https://github.com/skycoin/skywire/wiki/Skywire-Package-Installation) + +[Binary Releases](https://github.com/skycoin/skywire/releases) for many platforms and architectures are provided if none of the other installation methods are preferred. ## Dependencies @@ -117,33 +116,20 @@ git clone https://github.com/skycoin/skywire cd skywire #for the latest commits, check out the develop branch git checkout develop -make build1 +make build ``` -To compile skywire directly from source archive, first download the latest source archive from the release section with your browser or another utility. Extract it with an archiving utility, enter the directory where the sources were extracted, and run `make build1`. +To compile skywire directly from source archive, first download the latest source archive from the release section with your browser or another utility. Extract it with an archiving utility, enter the directory where the sources were extracted, and run `make build-merged` or `make build-merged-windows`. -`make build1` builds the binaries and apps with `go build` +`make build-merged` and `make build-merged-windows` builds a single binary containing all utilities and apps with `go build` -`skywire-cli` and `skywire-visor` binaries will populate in the current directory; app binaries will populate the `apps` directory. +the `skywire` binary will populate in the current directory. Build output: ``` -├──skywire-cli -└─┬skywire-visor - └─┬apps - ├──skychat - ├──skysocks - ├──skysocks-client - ├──vpn-client - ├──vpn-server - └──skychat -``` - -'install' these executables to the `GOPATH`: -``` -make install +└──skywire ``` For more options, run `make help`. @@ -153,14 +139,14 @@ For more options, run `make help`. To run skywire from this point, first generate a config. ``` -./skywire-cli config gen -birx +./skywire cli config gen -birx ``` `-b --bestproto` use the best protocol (dmsg | direct) to connect to the skywire production deployment `-i --ishv` create a local hypervisor configuration `-r --regen` regenerate a config which may already exist, retaining the keys `-x --retainhv` retain any remote hypervisors which are set in the config -More options for configuration are displayed with `./skywire-cli config gen -all`. +More options for configuration are displayed with `./skywire cli config gen -all`. ## Build docker image @@ -169,9 +155,9 @@ $ ./ci_scripts/docker-push.sh -t $(git rev-parse --abbrev-ref HEAD) -b ``` ## Skywire Configuration in-depth -The skywire visor requires a config file to run. This config is a json-formatted file produced by `skywire-cli config gen`. +The skywire visor requires a config file to run. This config is a json-formatted file produced by `skywire cli config gen`. -The `skywire-autoconfig` script included with the skywire package handles config generation and updates for the user who has installed the package. +The `skywire-autoconfig` script included with the skywire package handles config generation and updates for the user who installed the package. Examples of config generation and command / flag documentation can be found in the [cmd/skywire-cli/README.md](cmd/skywire-cli/README.md) and [cmd/skywire-visor/README.md](cmd/skywire-visor/README.md). @@ -182,14 +168,14 @@ The most important flags are noted below. In order to expose the hypervisor UI, generate a config file with `--is-hypervisor` or `-i` flag: ``` - skywire-cli config gen -i + skywire cli config gen -i ``` Docker container will create config automatically for you. To run it manually: ``` docker run --rm -v :/opt/skywire \ - skycoin/skywire:test skywire-cli config gen -i + skycoin/skywire:test skywire cli config gen -i ``` After starting up the visor, the UI will be exposed by default on `localhost:8000`. @@ -200,30 +186,25 @@ Every visor can be controlled by one or more hypervisors. To allow a hypervisor hypervisor needs to be specified in the configuration file. You can add a remote hypervisor to the config with: ``` -skywire-cli config update --hypervisor-pks +skywire cli config update --hypervisor-pks ``` OR: ``` -skywire-cli config gen --hvpk -``` - -Alternatively, this can be done with the skywire-autoconfg script included with the linux packages: -``` -skywire-autoconfig +skywire cli config gen --hvpk ``` Or from docker image: ``` docker run --rm -v :/opt/skywire \ - skycoin/skywire:test skywire-cli config update hypervisor-pks + skycoin/skywire:test skywire cli config update hypervisor-pks ``` Or from docker image:/* #nosec */ ``` docker run --rm -v :/opt/skywire \ - skycoin/skywire:latest skywire-cli update-config hypervisor-pks + skycoin/skywire:latest skywire cli update-config hypervisor-pks ``` @@ -252,81 +233,81 @@ _Note: not all of these files will be created by default._ Some of these files are served via the [dmsghttp logserver](https://github.com/skycoin/skywire/wiki/DMSGHTTP-logserver). -## Run skywire-visor +## Run `skywire visor` -`skywire-visor` hosts apps and is an applications gateway to the Skywire network. +`skywire visor` hosts apps and is an applications gateway to the Skywire network. -`skywire-visor` requires a valid configuration to be provided. +`skywire visor` requires a valid configuration to be provided. __Note: root permissions are currently required for vpn client and server applications!__ Run the visor: ``` - sudo skywire-visor -c skywire-config.json + sudo skywire visor -c skywire-config.json ``` If the default `skywire-config.json` exists in the current dir, this can be shortened to: ``` - sudo skywire-visor + sudo skywire visor ``` Or from docker image: ``` # with custom config mounted on docker volume -docker run --rm -p 8000:8000 -v :/opt/skywire --name=skywire skycoin/skywire:test skywire-visor -c /opt/skywire/.json +docker run --rm -p 8000:8000 -v :/opt/skywire --name=skywire skycoin/skywire:test skywire visor -c /opt/skywire/.json # without custom config (config is automatically generated) -docker run --rm -p 8000:8000 --name=skywire skycoin/skywire:test skywire-visor +docker run --rm -p 8000:8000 --name=skywire skycoin/skywire:test skywire visor ``` -`skywire-visor` can be run on Windows. The setup requires additional setup steps that are specified +`skywire visor` can be run on Windows. The setup requires additional setup steps that are specified in [the docs](docs/windows-setup.md) if not using the windows .msi. ## Run from source -Running from source as outlined in this section does not write the config to disk or explicitly compile any binaries. The config is piped from skywire-cli stdout to the visor stdin, and all are executed via `go run`. +Running from source as outlined in this section does not write the config to disk or explicitly compile any binaries. The config is piped from skywire cli stdout to the visor stdin, and all are executed via `go run`. ``` git clone https://github.com/skycoin/skywire.git cd skywire #for the latest commits, check out the develop branch git checkout develop -make run-source +make run-source-merged ``` ### Port forwarding over skywire -`skywire-cli fwd` is used to register and connect to http servers over the skywire connection +`skywire cli fwd` is used to register and connect to http servers over the skywire connection - [skywire forwarding](docs/skywire_forwarding.md) For example, if the local application you wish to forward is running on port `8080`: ``` -skywire-cli fwd -p 8080 +skywire cli fwd -p 8080 ``` List forwarded ports: ``` -skywire-cli fwd -l +skywire cli fwd -l ``` Deregister a port / turn off forwarding: ``` -skywire-cli fwd -d 8080 +skywire cli fwd -d 8080 ``` -To consume the skyfwd connection (i.e. reverse proxy back to localhost) use `skywire-cli rev` +To consume the skyfwd connection (i.e. reverse proxy back to localhost) use `skywire cli rev` A different port can be specified to proxy the remote port to: ``` -skywire-cli rev -p 8080 -r 8080 -k +skywire cli rev -p 8080 -r 8080 -k ``` List existing connections: ``` -skywire-cli rev -l +skywire cli rev -l ``` Remove a configured connection: ``` -skywire-cli rev -d +skywire cli rev -d ``` _Note: skyfwd is a new feature and could work more robustly. Issues are welcome._ @@ -350,17 +331,17 @@ To create a transport, first copy the public key of an online visor from the upt https://ut.skywire.skycoin.com/uptimes ``` -skywire-cli visor tp add -t +skywire cli visor tp add -t ``` View established transports: ``` -skywire-cli visor tp ls +skywire cli visor tp ls ``` Remove a transport: ``` -skywire-cli visor tp rm +skywire cli visor tp rm ``` ### Routing Rules @@ -375,13 +356,13 @@ To create a route, first copy the public key of an online visor from the uptime https://ut.skywire.skycoin.com/uptimes ``` -skywire-cli visor route add-rule app $(skywire-cli visor pk) +skywire cli visor route add-rule app $(skywire cli visor pk) ``` -To understand these arguments, observe the help menu for `skywire-cli visor route add-rule`: +To understand these arguments, observe the help menu for `skywire cli visor route add-rule`: ``` Usage: - skywire-cli visor route add-rule app \ + skywire cli visor route add-rule app \ \ \ \ @@ -413,10 +394,10 @@ The following documentation exists for vpn server / client setup and usage: - [Setup the Skywire VPN server](https://github.com/skycoin/skywire/wiki/Skywire-VPN-Server) - [Package Installation Guide](https://github.com/skycoin/skywire/wiki/Skywire-Package-Installation) -An example using the vpn with `skywire-cli`: +An example using the vpn with `skywire cli`: ``` -skywire-cli vpn list +skywire cli vpn list ``` This will query the service discovery for a list of vpn server public keys. [sd.skycoin.com/api/services?type=vpn](https://sd.skycoin.com/api/services?type=vpn) @@ -433,12 +414,12 @@ Sample output: Select a key and start the vpn with: ``` -skywire-cli vpn start +skywire cli vpn start ``` View the status of the vpn: ``` -skywire-cli vpn status +skywire cli vpn status ``` Check your ip address with ip.skywire.dev. @@ -446,7 +427,7 @@ __Note: ip.skycoin.com will only show your real ip address, not the ip address o Stop the vpn: ``` -skywire-cli vpn stop +skywire cli vpn stop ``` ### Using the Skywire SOCKS5 proxy client @@ -458,11 +439,11 @@ The following wiki documentation exists on the SOCKS5 proxy: The main difference between the vpn and the socks5 proxy is that the proxy is configured __per application__ while the vpn wraps the connections for the whole machine. -The socks client usage (from `skywire-cli`) is similar to the vpn, though the `skywire-cli` subcommands and flags do not currently match from the one application to the other. This will be rectified. +The socks client usage (from `skywire cli`) is similar to the vpn, though the `skywire cli` subcommands and flags do not currently match from the one application to the other. This will be rectified. -To use the SOCKS5 proxy client via `skywire-cli`: +To use the SOCKS5 proxy client via `skywire cli`: ``` -skywire-cli proxy list +skywire cli proxy list ``` This will query the service discovery for a list of visor public keys which are running the proxy server. [sd.skycoin.com/api/services?type=proxy](https://sd.skycoin.com/api/services?type=proxy) @@ -484,12 +465,12 @@ Sample output: Select a key and start the proxy with: ``` -skywire-cli proxy start --pk +skywire cli proxy start --pk ``` View the status of the proxy: ``` -skywire-cli proxy status +skywire cli proxy status ``` Check the ip address of the connection; for example, using `curl` via the socks5 proxy connection: @@ -515,7 +496,7 @@ ssh user@host -p 22 -o "ProxyCommand=ncat --proxy-type socks5 --proxy 127.0.0.1: Stop the socks5 proxy client: ``` -skywire-cli proxy stop +skywire cli proxy stop ``` ## Skycoin Rewards @@ -525,7 +506,7 @@ Review the [mainnet rules](/mainnet_rules.md) article for the details. Set a reward address: ``` -skywire-cli reward +skywire cli reward ``` Visors meeting uptime and eligability requirements will recieve daily skycoin rewards for up to 8 visors per location / ip address. diff --git a/cmd/apps/skychat/commands/skychat.go b/cmd/apps/skychat/commands/skychat.go new file mode 100644 index 000000000..b9d3c8dba --- /dev/null +++ b/cmd/apps/skychat/commands/skychat.go @@ -0,0 +1,337 @@ +// Package commands cmd/apps/skychat/skychat.go +package commands + +import ( + "context" + "embed" + "encoding/json" + "fmt" + "io/fs" + "log" + "net" + "net/http" + "os" + "os/signal" + "runtime" + "sync" + "time" + + ipc "github.com/james-barrow/golang-ipc" + "github.com/spf13/cobra" + + "github.com/skycoin/skywire-utilities/pkg/buildinfo" + "github.com/skycoin/skywire-utilities/pkg/cipher" + "github.com/skycoin/skywire-utilities/pkg/netutil" + "github.com/skycoin/skywire/pkg/app" + "github.com/skycoin/skywire/pkg/app/appnet" + "github.com/skycoin/skywire/pkg/app/appserver" + "github.com/skycoin/skywire/pkg/routing" + "github.com/skycoin/skywire/pkg/visor/visorconfig" +) + +const ( + netType = appnet.TypeSkynet + port = routing.Port(1) +) + +// var addr = flag.String("addr", ":8001", "address to bind, put an * before the port if you want to be able to access outside localhost") +var r = netutil.NewRetrier(nil, 50*time.Millisecond, netutil.DefaultMaxBackoff, 5, 2) + +var ( + addr string + appCl *app.Client + clientCh chan string + conns map[cipher.PubKey]net.Conn // Chat connections + connsMu sync.Mutex +) + +// the go embed static points to skywire/cmd/apps/skychat/static + +//go:embed static +var embededFiles embed.FS + +func init() { + RootCmd.Flags().StringVar(&addr, "addr", ":8001", "address to bind, put an * before the port if you want to be able to access outside localhost") +} + +// RootCmd is the root command for skywire-cli +var RootCmd = &cobra.Command{ + Use: "skychat", + Short: "skywire chat application", + Long: ` + ┌─┐┬┌─┬ ┬┌─┐┬ ┬┌─┐┌┬┐ + └─┐├┴┐└┬┘│ ├─┤├─┤ │ + └─┘┴ ┴ ┴ └─┘┴ ┴┴ ┴ ┴ `, + SilenceErrors: true, + SilenceUsage: true, + DisableSuggestions: true, + DisableFlagsInUseLine: true, + Version: buildinfo.Version(), + Run: func(cmd *cobra.Command, args []string) { + + appCl = app.NewClient(nil) + defer appCl.Close() + + if _, err := buildinfo.Get().WriteTo(os.Stdout); err != nil { + print(fmt.Sprintf("Failed to output build info: %v\n", err)) + } + + fmt.Println("Successfully started skychat.") + + clientCh = make(chan string) + defer close(clientCh) + + conns = make(map[cipher.PubKey]net.Conn) + go listenLoop() + + if runtime.GOOS == "windows" { + ipcClient, err := ipc.StartClient(visorconfig.SkychatName, nil) + if err != nil { + print(fmt.Sprintf("Error creating ipc server for skychat client: %v\n", err)) + setAppError(appCl, err) + os.Exit(1) + } + go handleIPCSignal(ipcClient) + } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + http.Handle("/", http.FileServer(getFileSystem())) + http.HandleFunc("/message", messageHandler(ctx)) + http.HandleFunc("/sse", sseHandler) + + url := "" + // address := *addr + address := addr + if len(address) < 5 || (address[:1] != ":" && address[:2] != "*:") { + url = "127.0.0.1:8001" + } else if address[:1] == ":" { + url = "127.0.0.1" + address + } else if address[:2] == "*:" { + url = address[1:] + } else { + url = "127.0.0.1:8001" + } + + fmt.Println("Serving HTTP on", url) + + if runtime.GOOS != "windows" { + termCh := make(chan os.Signal, 1) + signal.Notify(termCh, os.Interrupt) + + go func() { + <-termCh + setAppStatus(appCl, appserver.AppDetailedStatusStopped) + os.Exit(1) + }() + } + setAppStatus(appCl, appserver.AppDetailedStatusRunning) + srv := &http.Server{ //nolint gosec + Addr: url, + ReadTimeout: 5 * time.Second, + WriteTimeout: 10 * time.Second, + } + err := srv.ListenAndServe() + if err != nil { + print(err.Error()) + setAppError(appCl, err) + os.Exit(1) + } + + }, +} + +// Execute executes root CLI command. +func Execute() { + if err := RootCmd.Execute(); err != nil { + log.Fatal("Failed to execute command: ", err) + } +} + +func listenLoop() { + l, err := appCl.Listen(netType, port) + if err != nil { + print(fmt.Sprintf("Error listening network %v on port %d: %v\n", netType, port, err)) + setAppError(appCl, err) + return + } + + setAppPort(appCl, port) + + for { + fmt.Println("Accepting skychat conn...") + conn, err := l.Accept() + if err != nil { + print(fmt.Sprintf("Failed to accept conn: %v\n", err)) + return + } + fmt.Println("Accepted skychat conn") + + raddr := conn.RemoteAddr().(appnet.Addr) + connsMu.Lock() + conns[raddr.PubKey] = conn + connsMu.Unlock() + fmt.Printf("Accepted skychat conn on %s from %s\n", conn.LocalAddr(), raddr.PubKey) + + go handleConn(conn) + } +} + +func handleConn(conn net.Conn) { + raddr := conn.RemoteAddr().(appnet.Addr) + for { + buf := make([]byte, 32*1024) + n, err := conn.Read(buf) + if err != nil { + fmt.Println("Failed to read packet:", err) + raddr := conn.RemoteAddr().(appnet.Addr) + connsMu.Lock() + delete(conns, raddr.PubKey) + connsMu.Unlock() + return + } + + clientMsg, err := json.Marshal(map[string]string{"sender": raddr.PubKey.Hex(), "message": string(buf[:n])}) + if err != nil { + print(fmt.Sprintf("Failed to marshal json: %v\n", err)) + } + select { + case clientCh <- string(clientMsg): + fmt.Printf("Received and sent to ui: %s\n", clientMsg) + default: + fmt.Printf("Received and trashed: %s\n", clientMsg) + } + } +} + +func messageHandler(ctx context.Context) func(w http.ResponseWriter, rreq *http.Request) { + return func(w http.ResponseWriter, req *http.Request) { + + data := map[string]string{} + if err := json.NewDecoder(req.Body).Decode(&data); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + pk := cipher.PubKey{} + if err := pk.UnmarshalText([]byte(data["recipient"])); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + addr := appnet.Addr{ + Net: netType, + PubKey: pk, + Port: 1, + } + connsMu.Lock() + conn, ok := conns[pk] + connsMu.Unlock() + + if !ok { + var err error + err = r.Do(ctx, func() error { + conn, err = appCl.Dial(addr) + return err + }) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + connsMu.Lock() + conns[pk] = conn + connsMu.Unlock() + + go handleConn(conn) + } + + _, err := conn.Write([]byte(data["message"])) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + + connsMu.Lock() + delete(conns, pk) + connsMu.Unlock() + + return + } + } +} + +func sseHandler(w http.ResponseWriter, req *http.Request) { + f, ok := w.(http.Flusher) + if !ok { + http.Error(w, "Streaming unsupported!", http.StatusBadRequest) + return + } + + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + w.Header().Set("Transfer-Encoding", "chunked") + + for { + select { + case msg, ok := <-clientCh: + if !ok { + return + } + _, _ = fmt.Fprintf(w, "data: %s\n\n", msg) + f.Flush() + + case <-req.Context().Done(): + fmt.Print("SSE connection were closed.") + return + } + } +} + +func getFileSystem() http.FileSystem { + fsys, err := fs.Sub(embededFiles, "static") + if err != nil { + panic(err) + } + return http.FS(fsys) +} + +func handleIPCSignal(client *ipc.Client) { + time.Sleep(5 * time.Second) + if client == nil { + print(fmt.Sprintln("Unable to create IPC Client: server is non-existent")) + return + } + for { + m, err := client.Read() + if err != nil { + print(fmt.Sprintf("%s IPC received error: %v\n", visorconfig.SkychatName, err)) + } + + if m != nil { + if m.MsgType == visorconfig.IPCShutdownMessageType { + fmt.Println("Stopping " + visorconfig.SkychatName + " via IPC") + break + } + } + + } + client.Close() +} + +func setAppStatus(appCl *app.Client, status appserver.AppDetailedStatus) { + if err := appCl.SetDetailedStatus(string(status)); err != nil { + print(fmt.Sprintf("Failed to set status %v: %v\n", status, err)) + } +} + +func setAppError(appCl *app.Client, appErr error) { + if err := appCl.SetError(appErr.Error()); err != nil { + print(fmt.Sprintf("Failed to set error %v: %v\n", appErr, err)) + } +} + +func setAppPort(appCl *app.Client, port routing.Port) { + if err := appCl.SetAppPort(port); err != nil { + print(fmt.Sprintf("Failed to set port %v: %v\n", port, err)) + } +} diff --git a/cmd/apps/skychat/static/index.html b/cmd/apps/skychat/commands/static/index.html similarity index 100% rename from cmd/apps/skychat/static/index.html rename to cmd/apps/skychat/commands/static/index.html diff --git a/cmd/apps/skychat/static/p.png b/cmd/apps/skychat/commands/static/p.png similarity index 100% rename from cmd/apps/skychat/static/p.png rename to cmd/apps/skychat/commands/static/p.png diff --git a/cmd/apps/skychat/skychat.go b/cmd/apps/skychat/skychat.go index 8ed778a17..97cffd42e 100644 --- a/cmd/apps/skychat/skychat.go +++ b/cmd/apps/skychat/skychat.go @@ -1,303 +1,43 @@ -// /* cmd/apps/skychat/skychat.go -/* -skychat app for skywire visor -*/ +// Package main cmd/apps/skychat/skychat.go package main import ( - "context" - "embed" - "encoding/json" - "flag" - "fmt" - "io/fs" - "net" - "net/http" - "os" - "os/signal" - "runtime" - "sync" - "time" + cc "github.com/ivanpirog/coloredcobra" + "github.com/spf13/cobra" - ipc "github.com/james-barrow/golang-ipc" - - "github.com/skycoin/skywire-utilities/pkg/buildinfo" - "github.com/skycoin/skywire-utilities/pkg/cipher" - "github.com/skycoin/skywire-utilities/pkg/netutil" - "github.com/skycoin/skywire/pkg/app" - "github.com/skycoin/skywire/pkg/app/appnet" - "github.com/skycoin/skywire/pkg/app/appserver" - "github.com/skycoin/skywire/pkg/routing" - "github.com/skycoin/skywire/pkg/visor/visorconfig" -) - -const ( - netType = appnet.TypeSkynet - port = routing.Port(1) + "github.com/skycoin/skywire/cmd/apps/skychat/commands" ) -var addr = flag.String("addr", ":8001", "address to bind, put an * before the port if you want to be able to access outside localhost") -var r = netutil.NewRetrier(nil, 50*time.Millisecond, netutil.DefaultMaxBackoff, 5, 2) - -var ( - appCl *app.Client - clientCh chan string - conns map[cipher.PubKey]net.Conn // Chat connections - connsMu sync.Mutex -) - -// the go embed static points to skywire/cmd/apps/skychat/static - -//go:embed static -var embededFiles embed.FS - -func main() { - appCl = app.NewClient(nil) - defer appCl.Close() - - if _, err := buildinfo.Get().WriteTo(os.Stdout); err != nil { - print(fmt.Sprintf("Failed to output build info: %v\n", err)) - } - - flag.Parse() - fmt.Println("Successfully started skychat.") - - clientCh = make(chan string) - defer close(clientCh) - - conns = make(map[cipher.PubKey]net.Conn) - go listenLoop() - - if runtime.GOOS == "windows" { - ipcClient, err := ipc.StartClient(visorconfig.SkychatName, nil) - if err != nil { - print(fmt.Sprintf("Error creating ipc server for skychat client: %v\n", err)) - setAppError(appCl, err) - os.Exit(1) - } - go handleIPCSignal(ipcClient) - } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - http.Handle("/", http.FileServer(getFileSystem())) - http.HandleFunc("/message", messageHandler(ctx)) - http.HandleFunc("/sse", sseHandler) - - url := "" - address := *addr - if len(address) < 5 || (address[:1] != ":" && address[:2] != "*:") { - url = "127.0.0.1:8001" - } else if address[:1] == ":" { - url = "127.0.0.1" + address - } else if address[:2] == "*:" { - url = address[1:] - } else { - url = "127.0.0.1:8001" - } - - fmt.Println("Serving HTTP on", url) - - if runtime.GOOS != "windows" { - termCh := make(chan os.Signal, 1) - signal.Notify(termCh, os.Interrupt) - - go func() { - <-termCh - setAppStatus(appCl, appserver.AppDetailedStatusStopped) - os.Exit(1) - }() - } - setAppStatus(appCl, appserver.AppDetailedStatusRunning) - srv := &http.Server{ //nolint gosec - Addr: url, - ReadTimeout: 5 * time.Second, - WriteTimeout: 10 * time.Second, - } - err := srv.ListenAndServe() - if err != nil { - print(err.Error()) - setAppError(appCl, err) - os.Exit(1) - } - -} - -func listenLoop() { - l, err := appCl.Listen(netType, port) - if err != nil { - print(fmt.Sprintf("Error listening network %v on port %d: %v\n", netType, port, err)) - setAppError(appCl, err) - return - } - - setAppPort(appCl, port) - - for { - fmt.Println("Accepting skychat conn...") - conn, err := l.Accept() - if err != nil { - print(fmt.Sprintf("Failed to accept conn: %v\n", err)) - return - } - fmt.Println("Accepted skychat conn") - - raddr := conn.RemoteAddr().(appnet.Addr) - connsMu.Lock() - conns[raddr.PubKey] = conn - connsMu.Unlock() - fmt.Printf("Accepted skychat conn on %s from %s\n", conn.LocalAddr(), raddr.PubKey) - - go handleConn(conn) - } +func init() { + var helpflag bool + commands.RootCmd.SetUsageTemplate(help) + commands.RootCmd.PersistentFlags().BoolVarP(&helpflag, "help", "h", false, "help menu") + commands.RootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) + commands.RootCmd.PersistentFlags().MarkHidden("help") //nolint } -func handleConn(conn net.Conn) { - raddr := conn.RemoteAddr().(appnet.Addr) - for { - buf := make([]byte, 32*1024) - n, err := conn.Read(buf) - if err != nil { - fmt.Println("Failed to read packet:", err) - raddr := conn.RemoteAddr().(appnet.Addr) - connsMu.Lock() - delete(conns, raddr.PubKey) - connsMu.Unlock() - return - } - - clientMsg, err := json.Marshal(map[string]string{"sender": raddr.PubKey.Hex(), "message": string(buf[:n])}) - if err != nil { - print(fmt.Sprintf("Failed to marshal json: %v\n", err)) - } - select { - case clientCh <- string(clientMsg): - fmt.Printf("Received and sent to ui: %s\n", clientMsg) - default: - fmt.Printf("Received and trashed: %s\n", clientMsg) - } - } -} - -func messageHandler(ctx context.Context) func(w http.ResponseWriter, rreq *http.Request) { - return func(w http.ResponseWriter, req *http.Request) { - - data := map[string]string{} - if err := json.NewDecoder(req.Body).Decode(&data); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - pk := cipher.PubKey{} - if err := pk.UnmarshalText([]byte(data["recipient"])); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - addr := appnet.Addr{ - Net: netType, - PubKey: pk, - Port: 1, - } - connsMu.Lock() - conn, ok := conns[pk] - connsMu.Unlock() - - if !ok { - var err error - err = r.Do(ctx, func() error { - conn, err = appCl.Dial(addr) - return err - }) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - connsMu.Lock() - conns[pk] = conn - connsMu.Unlock() - - go handleConn(conn) - } - - _, err := conn.Write([]byte(data["message"])) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - - connsMu.Lock() - delete(conns, pk) - connsMu.Unlock() - - return - } - } -} - -func sseHandler(w http.ResponseWriter, req *http.Request) { - f, ok := w.(http.Flusher) - if !ok { - http.Error(w, "Streaming unsupported!", http.StatusBadRequest) - return - } - - w.Header().Set("Content-Type", "text/event-stream") - w.Header().Set("Cache-Control", "no-cache") - w.Header().Set("Connection", "keep-alive") - w.Header().Set("Transfer-Encoding", "chunked") - - for { - select { - case msg, ok := <-clientCh: - if !ok { - return - } - _, _ = fmt.Fprintf(w, "data: %s\n\n", msg) - f.Flush() - - case <-req.Context().Done(): - fmt.Print("SSE connection were closed.") - return - } - } -} - -func getFileSystem() http.FileSystem { - fsys, err := fs.Sub(embededFiles, "static") - if err != nil { - panic(err) - } - return http.FS(fsys) -} - -func handleIPCSignal(client *ipc.Client) { - for { - m, err := client.Read() - if err != nil { - fmt.Printf("%s IPC received error: %v", visorconfig.SkychatName, err) - } - if m.MsgType == visorconfig.IPCShutdownMessageType { - fmt.Println("Stopping " + visorconfig.SkychatName + " via IPC") - break - } - } - os.Exit(0) -} - -func setAppStatus(appCl *app.Client, status appserver.AppDetailedStatus) { - if err := appCl.SetDetailedStatus(string(status)); err != nil { - print(fmt.Sprintf("Failed to set status %v: %v\n", status, err)) - } -} - -func setAppError(appCl *app.Client, appErr error) { - if err := appCl.SetError(appErr.Error()); err != nil { - print(fmt.Sprintf("Failed to set error %v: %v\n", appErr, err)) - } -} - -func setAppPort(appCl *app.Client, port routing.Port) { - if err := appCl.SetAppPort(port); err != nil { - print(fmt.Sprintf("Failed to set port %v: %v\n", port, err)) - } -} +func main() { + cc.Init(&cc.Config{ + RootCmd: commands.RootCmd, + Headings: cc.HiBlue + cc.Bold, + Commands: cc.HiBlue + cc.Bold, + CmdShortDescr: cc.HiBlue, + Example: cc.HiBlue + cc.Italic, + ExecName: cc.HiBlue + cc.Bold, + Flags: cc.HiBlue + cc.Bold, + FlagsDescr: cc.HiBlue, + NoExtraNewlines: true, + NoBottomNewline: true, + }) + commands.Execute() +} + +const help = "Usage:\r\n" + + " {{.UseLine}}{{if .HasAvailableSubCommands}}{{end}} {{if gt (len .Aliases) 0}}\r\n\r\n" + + "{{.NameAndAliases}}{{end}}{{if .HasAvailableSubCommands}}\r\n\r\n" + + "Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand)}}\r\n " + + "{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}\r\n\r\n" + + "Flags:\r\n" + + "{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}\r\n\r\n" + + "Global Flags:\r\n" + + "{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}\r\n\r\n" diff --git a/cmd/apps/skysocks-client/commands/skysocks-client.go b/cmd/apps/skysocks-client/commands/skysocks-client.go new file mode 100644 index 000000000..3c5332a00 --- /dev/null +++ b/cmd/apps/skysocks-client/commands/skysocks-client.go @@ -0,0 +1,206 @@ +// Package commands cmd/apps/skysocks-client/skysocks-client.go +package commands + +import ( + "context" + "errors" + "fmt" + "log" + "net" + "net/http" + "net/url" + "os" + "os/signal" + "runtime" + "time" + + "github.com/elazarl/goproxy" + ipc "github.com/james-barrow/golang-ipc" + "github.com/spf13/cobra" + + "github.com/skycoin/skywire-utilities/pkg/buildinfo" + "github.com/skycoin/skywire-utilities/pkg/cipher" + "github.com/skycoin/skywire-utilities/pkg/netutil" + "github.com/skycoin/skywire/internal/skysocks" + "github.com/skycoin/skywire/pkg/app" + "github.com/skycoin/skywire/pkg/app/appnet" + "github.com/skycoin/skywire/pkg/app/appserver" + "github.com/skycoin/skywire/pkg/routing" + "github.com/skycoin/skywire/pkg/visor/visorconfig" +) + +const ( + netType = appnet.TypeSkynet + socksPort = routing.Port(3) +) + +var ( + r = netutil.NewRetrier(nil, time.Second, netutil.DefaultMaxBackoff, 0, 1) + addr string + serverPK string + httpAddr string +) + +func init() { + RootCmd.Flags().StringVar(&addr, "addr", visorconfig.SkysocksClientAddr, "Client address to listen on") + RootCmd.Flags().StringVar(&serverPK, "srv", "", "PubKey of the server to connect to") + RootCmd.Flags().StringVar(&httpAddr, "http", "", "http proxy mode") +} + +// RootCmd is the root command for skysocks +var RootCmd = &cobra.Command{ + Use: "skysocks-client", + Short: "skywire socks5 proxy client application", + Long: ` + ┌─┐┬┌─┬ ┬┌─┐┌─┐┌─┐┬┌─┌─┐ ┌─┐┬ ┬┌─┐┌┐┌┌┬┐ + └─┐├┴┐└┬┘└─┐│ ││ ├┴┐└─┐───│ │ │├┤ │││ │ + └─┘┴ ┴ ┴ └─┘└─┘└─┘┴ ┴└─┘ └─┘┴─┘┴└─┘┘└┘ ┴ `, + SilenceErrors: true, + SilenceUsage: true, + DisableSuggestions: true, + DisableFlagsInUseLine: true, + Version: buildinfo.Version(), + Run: func(cmd *cobra.Command, args []string) { + appCl := app.NewClient(nil) + defer appCl.Close() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + if _, err := buildinfo.Get().WriteTo(os.Stdout); err != nil { + print(fmt.Sprintf("Failed to output build info: %v\n", err)) + } + + if serverPK == "" { + err := errors.New("Empty server PubKey. Exiting") + print(fmt.Sprintf("%v\n", err)) + setAppErr(appCl, err) + os.Exit(1) + } + + pk := cipher.PubKey{} + if err := pk.UnmarshalText([]byte(serverPK)); err != nil { + print(fmt.Sprintf("Invalid server PubKey: %v\n", err)) + setAppErr(appCl, err) + os.Exit(1) + } + + defer setAppStatus(appCl, appserver.AppDetailedStatusStopped) + setAppPort(appCl, appCl.Config().RoutingPort) + + conn, err := dialServer(ctx, appCl, pk) + if err != nil { + print(fmt.Sprintf("Failed to dial to a server: %v\n", err)) + setAppErr(appCl, err) + os.Exit(1) + } + + fmt.Printf("Connected to %v\n", pk) + client, err := skysocks.NewClient(conn, appCl) + if err != nil { + print(fmt.Sprintf("Failed to create a new client: %v\n", err)) + setAppErr(appCl, err) + os.Exit(1) + } + if runtime.GOOS == "windows" { + ipcClient, err := ipc.StartClient(visorconfig.SkysocksClientName, nil) + if err != nil { + setAppErr(appCl, err) + print(fmt.Sprintf("Error creating ipc server for skysocks: %v\n", err)) + os.Exit(1) + } + go client.ListenIPC(ipcClient) + } else { + termCh := make(chan os.Signal, 1) + signal.Notify(termCh, os.Interrupt) + go func() { + <-termCh + if err := client.Close(); err != nil { + print(fmt.Sprintf("%v\n", err)) + os.Exit(1) + } + }() + } + + fmt.Printf("Serving proxy client %v\n", addr) + setAppStatus(appCl, appserver.AppDetailedStatusRunning) + httpCtx, httpCancel := context.WithCancel(ctx) + if httpAddr != "" { + go httpProxy(httpCtx, httpAddr, addr) + } + defer httpCancel() + if err := client.ListenAndServe(addr); err != nil { + print(fmt.Sprintf("Error serving proxy client: %v\n", err)) + } + setAppStatus(appCl, appserver.AppDetailedStatusStopped) + }, +} + +func dialServer(ctx context.Context, appCl *app.Client, pk cipher.PubKey) (net.Conn, error) { + appCl.SetDetailedStatus(appserver.AppDetailedStatusStarting) //nolint + var conn net.Conn + err := r.Do(ctx, func() error { + var err error + conn, err = appCl.Dial(appnet.Addr{ + Net: netType, + PubKey: pk, + Port: socksPort, + }) + return err + }) + if err != nil { + return nil, err + } + + return conn, nil +} + +func setAppErr(appCl *app.Client, err error) { + if appErr := appCl.SetError(err.Error()); appErr != nil { + print(fmt.Sprintf("Failed to set error %v: %v\n", err, appErr)) + } +} + +func setAppStatus(appCl *app.Client, status appserver.AppDetailedStatus) { + if err := appCl.SetDetailedStatus(string(status)); err != nil { + print(fmt.Sprintf("Failed to set status %v: %v\n", status, err)) + } +} + +func setAppPort(appCl *app.Client, port routing.Port) { + if err := appCl.SetAppPort(port); err != nil { + print(fmt.Sprintf("Failed to set port %v: %v\n", port, err)) + } +} + +func httpProxy(ctx context.Context, httpAddr, sockscAddr string) { + proxy := goproxy.NewProxyHttpServer() + + proxyURL, err := url.Parse(fmt.Sprintf("socks5://127.0.0.1%s", sockscAddr)) //nolint + if err != nil { + print(fmt.Sprintf("Failed to parse socks address: %v\n", err)) + return + } + + proxy.Tr.Proxy = http.ProxyURL(proxyURL) + + fmt.Printf("Serving http proxy %v\n", httpAddr) + httpProxySrv := &http.Server{Addr: httpAddr, Handler: proxy} //nolint + + go func() { + <-ctx.Done() + httpProxySrv.Close() //nolint + print("Stopping http proxy") + }() + + if err := httpProxySrv.ListenAndServe(); err != nil { //nolint + print(fmt.Sprintf("Error serving http proxy: %v\n", err)) + } +} + +// Execute executes root CLI command. +func Execute() { + if err := RootCmd.Execute(); err != nil { + log.Fatal("Failed to execute command: ", err) + } +} diff --git a/cmd/apps/skysocks-client/skysocks-client.go b/cmd/apps/skysocks-client/skysocks-client.go index 34cea6f82..c4bd7cc3f 100644 --- a/cmd/apps/skysocks-client/skysocks-client.go +++ b/cmd/apps/skysocks-client/skysocks-client.go @@ -1,133 +1,43 @@ -// /* cmd/apps/skysocks-client/skysocks-client.go -/* -proxy client app for skywire visor -*/ +// Package main cmd/apps/skysocks-client/skysocks-client.go package main import ( - "context" - "errors" - "flag" - "fmt" - "io" - "net" - "os" - "time" + cc "github.com/ivanpirog/coloredcobra" + "github.com/spf13/cobra" - "github.com/skycoin/skywire-utilities/pkg/buildinfo" - "github.com/skycoin/skywire-utilities/pkg/cipher" - "github.com/skycoin/skywire-utilities/pkg/netutil" - "github.com/skycoin/skywire/internal/skysocks" - "github.com/skycoin/skywire/pkg/app" - "github.com/skycoin/skywire/pkg/app/appnet" - "github.com/skycoin/skywire/pkg/app/appserver" - "github.com/skycoin/skywire/pkg/routing" - "github.com/skycoin/skywire/pkg/visor/visorconfig" + "github.com/skycoin/skywire/cmd/apps/skysocks-client/commands" ) -const ( - netType = appnet.TypeSkynet - socksPort = routing.Port(3) -) - -var r = netutil.NewRetrier(nil, time.Second, netutil.DefaultMaxBackoff, 0, 1) - -func dialServer(ctx context.Context, appCl *app.Client, pk cipher.PubKey) (net.Conn, error) { - appCl.SetDetailedStatus(appserver.AppDetailedStatusStarting) //nolint - var conn net.Conn - err := r.Do(ctx, func() error { - var err error - conn, err = appCl.Dial(appnet.Addr{ - Net: netType, - PubKey: pk, - Port: socksPort, - }) - return err - }) - if err != nil { - return nil, err - } - - return conn, nil +func init() { + var helpflag bool + commands.RootCmd.SetUsageTemplate(help) + commands.RootCmd.PersistentFlags().BoolVarP(&helpflag, "help", "h", false, "help menu") + commands.RootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) + commands.RootCmd.PersistentFlags().MarkHidden("help") //nolint } func main() { - appCl := app.NewClient(nil) - defer appCl.Close() - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - if _, err := buildinfo.Get().WriteTo(os.Stdout); err != nil { - print(fmt.Sprintf("Failed to output build info: %v\n", err)) - } - - var addr = flag.String("addr", visorconfig.SkysocksClientAddr, "Client address to listen on") - var serverPK = flag.String("srv", "", "PubKey of the server to connect to") - flag.Parse() - - if *serverPK == "" { - err := errors.New("Empty server PubKey. Exiting") - print(fmt.Sprintf("%v\n", err)) - setAppErr(appCl, err) - os.Exit(1) - } - - pk := cipher.PubKey{} - if err := pk.UnmarshalText([]byte(*serverPK)); err != nil { - print(fmt.Sprintf("Invalid server PubKey: %v\n", err)) - setAppErr(appCl, err) - os.Exit(1) - } - defer setAppStatus(appCl, appserver.AppDetailedStatusStopped) - setAppPort(appCl, appCl.Config().RoutingPort) - for { - conn, err := dialServer(ctx, appCl, pk) - if err != nil { - print(fmt.Sprintf("Failed to dial to a server: %v\n", err)) - setAppErr(appCl, err) - os.Exit(1) - } - - fmt.Printf("Connected to %v\n", pk) - client, err := skysocks.NewClient(conn, appCl) - if err != nil { - print(fmt.Sprintf("Failed to create a new client: %v\n", err)) - setAppErr(appCl, err) - os.Exit(1) - } - - fmt.Printf("Serving proxy client %v\n", *addr) - setAppStatus(appCl, appserver.AppDetailedStatusRunning) - - if err := client.ListenAndServe(*addr); err != nil { - print(fmt.Sprintf("Error serving proxy client: %v\n", err)) - } - - // need to filter this out, cause usually client failure means app conn is already closed - if err := conn.Close(); err != nil && err != io.ErrClosedPipe { - print(fmt.Sprintf("Error closing app conn: %v\n", err)) - } - - fmt.Println("Reconnecting to skysocks server") - setAppStatus(appCl, appserver.AppDetailedStatusReconnecting) - } -} - -func setAppErr(appCl *app.Client, err error) { - if appErr := appCl.SetError(err.Error()); appErr != nil { - print(fmt.Sprintf("Failed to set error %v: %v\n", err, appErr)) - } -} - -func setAppStatus(appCl *app.Client, status appserver.AppDetailedStatus) { - if err := appCl.SetDetailedStatus(string(status)); err != nil { - print(fmt.Sprintf("Failed to set status %v: %v\n", status, err)) - } + cc.Init(&cc.Config{ + RootCmd: commands.RootCmd, + Headings: cc.HiBlue + cc.Bold, + Commands: cc.HiBlue + cc.Bold, + CmdShortDescr: cc.HiBlue, + Example: cc.HiBlue + cc.Italic, + ExecName: cc.HiBlue + cc.Bold, + Flags: cc.HiBlue + cc.Bold, + FlagsDescr: cc.HiBlue, + NoExtraNewlines: true, + NoBottomNewline: true, + }) + commands.Execute() } -func setAppPort(appCl *app.Client, port routing.Port) { - if err := appCl.SetAppPort(port); err != nil { - print(fmt.Sprintf("Failed to set port %v: %v\n", port, err)) - } -} +const help = "Usage:\r\n" + + " {{.UseLine}}{{if .HasAvailableSubCommands}}{{end}} {{if gt (len .Aliases) 0}}\r\n\r\n" + + "{{.NameAndAliases}}{{end}}{{if .HasAvailableSubCommands}}\r\n\r\n" + + "Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand)}}\r\n " + + "{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}\r\n\r\n" + + "Flags:\r\n" + + "{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}\r\n\r\n" + + "Global Flags:\r\n" + + "{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}\r\n\r\n" diff --git a/cmd/apps/skysocks/commands/skysocks.go b/cmd/apps/skysocks/commands/skysocks.go new file mode 100644 index 000000000..223af2b02 --- /dev/null +++ b/cmd/apps/skysocks/commands/skysocks.go @@ -0,0 +1,126 @@ +// Package commands cmd/apps/skysocks/skysocks.go +package commands + +import ( + "fmt" + "log" + "os" + "os/signal" + "runtime" + + ipc "github.com/james-barrow/golang-ipc" + "github.com/spf13/cobra" + + "github.com/skycoin/skywire-utilities/pkg/buildinfo" + "github.com/skycoin/skywire/internal/skysocks" + "github.com/skycoin/skywire/pkg/app" + "github.com/skycoin/skywire/pkg/app/appnet" + "github.com/skycoin/skywire/pkg/app/appserver" + "github.com/skycoin/skywire/pkg/routing" + "github.com/skycoin/skywire/pkg/visor/visorconfig" +) + +const ( + netType = appnet.TypeSkynet + port = routing.Port(3) +) + +var passcode string + +func init() { + RootCmd.Flags().StringVar(&passcode, "passcode", "", "passcode to authenticate connecting users") +} + +// RootCmd is the root command for skysocks +var RootCmd = &cobra.Command{ + Use: "skysocks", + Short: "skywire socks5 proxy server application", + Long: ` + ┌─┐┬┌─┬ ┬┌─┐┌─┐┌─┐┬┌─┌─┐ + └─┐├┴┐└┬┘└─┐│ ││ ├┴┐└─┐ + └─┘┴ ┴ ┴ └─┘└─┘└─┘┴ ┴└─┘`, + SilenceErrors: true, + SilenceUsage: true, + DisableSuggestions: true, + DisableFlagsInUseLine: true, + Version: buildinfo.Version(), + Run: func(cmd *cobra.Command, args []string) { + appCl := app.NewClient(nil) + defer appCl.Close() + + if _, err := buildinfo.Get().WriteTo(os.Stdout); err != nil { + print(fmt.Sprintf("Failed to output build info: %v", err)) + } + + srv, err := skysocks.NewServer(passcode, appCl) + if err != nil { + setAppError(appCl, err) + print(fmt.Sprintf("Failed to create a new server: %v\n", err)) + os.Exit(1) + } + + l, err := appCl.Listen(netType, port) + if err != nil { + setAppError(appCl, err) + print(fmt.Sprintf("Error listening network %v on port %d: %v\n", netType, port, err)) + os.Exit(1) + } + + setAppPort(appCl, port) + + fmt.Println("Starting serving proxy server") + + if runtime.GOOS == "windows" { + ipcClient, err := ipc.StartClient(visorconfig.SkysocksName, nil) + if err != nil { + setAppError(appCl, err) + print(fmt.Sprintf("Error creating ipc server for skysocks: %v\n", err)) + os.Exit(1) + } + go srv.ListenIPC(ipcClient) + } else { + termCh := make(chan os.Signal, 1) + signal.Notify(termCh, os.Interrupt) + + go func() { + <-termCh + + if err := srv.Close(); err != nil { + print(fmt.Sprintf("%v\n", err)) + os.Exit(1) + } + }() + } + defer setAppStatus(appCl, appserver.AppDetailedStatusStopped) + + if err := srv.Serve(l); err != nil { + print(fmt.Sprintf("%v\n", err)) + os.Exit(1) + } + }, +} + +func setAppStatus(appCl *app.Client, status appserver.AppDetailedStatus) { + if err := appCl.SetDetailedStatus(string(status)); err != nil { + print(fmt.Sprintf("Failed to set status %v: %v\n", status, err)) + } +} + +func setAppError(appCl *app.Client, appErr error) { + if err := appCl.SetError(appErr.Error()); err != nil { + print(fmt.Sprintf("Failed to set error %v: %v\n", appErr, err)) + } +} + +func setAppPort(appCl *app.Client, port routing.Port) { + if err := appCl.SetAppPort(port); err != nil { + print(fmt.Sprintf("Failed to set port %v: %v\n", port, err)) + } +} + +// Execute executes root CLI command. +func Execute() { + if err := RootCmd.Execute(); err != nil { + log.Fatal("Failed to execute command: ", err) + } +} diff --git a/cmd/apps/skysocks/skysocks.go b/cmd/apps/skysocks/skysocks.go index 6ca34c066..1dff53684 100644 --- a/cmd/apps/skysocks/skysocks.go +++ b/cmd/apps/skysocks/skysocks.go @@ -1,104 +1,43 @@ -// /* cmd/apps/skysocks/skysocks.go -/* -proxy server app for skywire visor -*/ +// Package main cmd/apps/skysocks/skysocks.go package main import ( - "flag" - "fmt" - "os" - "os/signal" - "runtime" + cc "github.com/ivanpirog/coloredcobra" + "github.com/spf13/cobra" - ipc "github.com/james-barrow/golang-ipc" - - "github.com/skycoin/skywire-utilities/pkg/buildinfo" - "github.com/skycoin/skywire/internal/skysocks" - "github.com/skycoin/skywire/pkg/app" - "github.com/skycoin/skywire/pkg/app/appnet" - "github.com/skycoin/skywire/pkg/app/appserver" - "github.com/skycoin/skywire/pkg/routing" - "github.com/skycoin/skywire/pkg/visor/visorconfig" -) - -const ( - netType = appnet.TypeSkynet - port = routing.Port(3) + "github.com/skycoin/skywire/cmd/apps/skysocks/commands" ) -func main() { - appCl := app.NewClient(nil) - defer appCl.Close() - - if _, err := buildinfo.Get().WriteTo(os.Stdout); err != nil { - print(fmt.Sprintf("Failed to output build info: %v", err)) - } - - var passcode = flag.String("passcode", "", "Authorize user against this passcode") - flag.Parse() - - srv, err := skysocks.NewServer(*passcode, appCl) - if err != nil { - setAppError(appCl, err) - print(fmt.Sprintf("Failed to create a new server: %v\n", err)) - os.Exit(1) - } - - l, err := appCl.Listen(netType, port) - if err != nil { - setAppError(appCl, err) - print(fmt.Sprintf("Error listening network %v on port %d: %v\n", netType, port, err)) - os.Exit(1) - } - - setAppPort(appCl, port) - - fmt.Println("Starting serving proxy server") - - if runtime.GOOS == "windows" { - ipcClient, err := ipc.StartClient(visorconfig.VPNClientName, nil) - if err != nil { - setAppError(appCl, err) - print(fmt.Sprintf("Error creating ipc server for VPN client: %v\n", err)) - os.Exit(1) - } - go srv.ListenIPC(ipcClient) - } else { - termCh := make(chan os.Signal, 1) - signal.Notify(termCh, os.Interrupt) - - go func() { - <-termCh - - if err := srv.Close(); err != nil { - print(fmt.Sprintf("%v\n", err)) - os.Exit(1) - } - }() - } - defer setAppStatus(appCl, appserver.AppDetailedStatusStopped) - - if err := srv.Serve(l); err != nil { - print(fmt.Sprintf("%v\n", err)) - os.Exit(1) - } +func init() { + var helpflag bool + commands.RootCmd.SetUsageTemplate(help) + commands.RootCmd.PersistentFlags().BoolVarP(&helpflag, "help", "h", false, "help menu") + commands.RootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) + commands.RootCmd.PersistentFlags().MarkHidden("help") //nolint } -func setAppStatus(appCl *app.Client, status appserver.AppDetailedStatus) { - if err := appCl.SetDetailedStatus(string(status)); err != nil { - print(fmt.Sprintf("Failed to set status %v: %v\n", status, err)) - } -} - -func setAppError(appCl *app.Client, appErr error) { - if err := appCl.SetError(appErr.Error()); err != nil { - print(fmt.Sprintf("Failed to set error %v: %v\n", appErr, err)) - } +func main() { + cc.Init(&cc.Config{ + RootCmd: commands.RootCmd, + Headings: cc.HiBlue + cc.Bold, + Commands: cc.HiBlue + cc.Bold, + CmdShortDescr: cc.HiBlue, + Example: cc.HiBlue + cc.Italic, + ExecName: cc.HiBlue + cc.Bold, + Flags: cc.HiBlue + cc.Bold, + FlagsDescr: cc.HiBlue, + NoExtraNewlines: true, + NoBottomNewline: true, + }) + commands.Execute() } -func setAppPort(appCl *app.Client, port routing.Port) { - if err := appCl.SetAppPort(port); err != nil { - print(fmt.Sprintf("Failed to set port %v: %v\n", port, err)) - } -} +const help = "Usage:\r\n" + + " {{.UseLine}}{{if .HasAvailableSubCommands}}{{end}} {{if gt (len .Aliases) 0}}\r\n\r\n" + + "{{.NameAndAliases}}{{end}}{{if .HasAvailableSubCommands}}\r\n\r\n" + + "Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand)}}\r\n " + + "{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}\r\n\r\n" + + "Flags:\r\n" + + "{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}\r\n\r\n" + + "Global Flags:\r\n" + + "{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}\r\n\r\n" diff --git a/cmd/apps/vpn-client/commands/vpn-client.go b/cmd/apps/vpn-client/commands/vpn-client.go new file mode 100644 index 000000000..a93374dc9 --- /dev/null +++ b/cmd/apps/vpn-client/commands/vpn-client.go @@ -0,0 +1,244 @@ +// Package commands vpn-client.go +package commands + +import ( + "errors" + "fmt" + "log" + "net" + "os" + "os/signal" + "runtime" + "syscall" + + ipc "github.com/james-barrow/golang-ipc" + "github.com/spf13/cobra" + + "github.com/skycoin/skywire-utilities/pkg/buildinfo" + "github.com/skycoin/skywire-utilities/pkg/cipher" + "github.com/skycoin/skywire/internal/vpn" + "github.com/skycoin/skywire/pkg/app" + "github.com/skycoin/skywire/pkg/app/appevent" + "github.com/skycoin/skywire/pkg/app/appserver" + "github.com/skycoin/skywire/pkg/routing" + "github.com/skycoin/skywire/pkg/visor/visorconfig" +) + +var ( + serverPKStr string + localPKStr string + localSKStr string + passcode string + killswitch bool + dnsAddr string +) + +func init() { + RootCmd.Flags().StringVar(&serverPKStr, "srv", "", "PubKey of the server to connect to") + RootCmd.Flags().StringVar(&localPKStr, "pk", "", "local pubkey") + RootCmd.Flags().StringVar(&localSKStr, "sk", "", "local seckey") + RootCmd.Flags().StringVar(&passcode, "passcode", "", "passcode to authenticate connection") + RootCmd.Flags().BoolVar(&killswitch, "killswitch", false, "If set, the Internet won't be restored during reconnection attempts") + RootCmd.Flags().StringVar(&dnsAddr, "dns", "", "address of DNS want set to tun") +} + +// RootCmd is the root command for skywire-cli +var RootCmd = &cobra.Command{ + Use: "vpn-client", + Short: "skywire vpn client application", + Long: ` + ┬ ┬┌─┐┌┐┌ ┌─┐┬ ┬┌─┐┌┐┌┌┬┐ + └┐┌┘├─┘│││───│ │ │├┤ │││ │ + └┘ ┴ ┘└┘ └─┘┴─┘┴└─┘┘└┘ ┴ `, + SilenceErrors: true, + SilenceUsage: true, + DisableSuggestions: true, + DisableFlagsInUseLine: true, + Version: buildinfo.Version(), + Run: func(cmd *cobra.Command, args []string) { + + var directIPsCh, nonDirectIPsCh = make(chan net.IP, 100), make(chan net.IP, 100) + defer close(directIPsCh) + defer close(nonDirectIPsCh) + + eventSub := appevent.NewSubscriber() + + parseIP := func(addr string) net.IP { + ip, ok, err := vpn.ParseIP(addr) + if err != nil { + print(fmt.Sprintf("Failed to parse IP %s: %v\n", addr, err)) + return nil + } + if !ok { + print(fmt.Sprintf("Failed to parse IP %s\n", addr)) + return nil + } + + return ip + } + + eventSub.OnTCPDial(func(data appevent.TCPDialData) { + if ip := parseIP(data.RemoteAddr); ip != nil { + directIPsCh <- ip + } + }) + + eventSub.OnTCPClose(func(data appevent.TCPCloseData) { + if ip := parseIP(data.RemoteAddr); ip != nil { + nonDirectIPsCh <- ip + } + }) + + appCl := app.NewClient(eventSub) + defer appCl.Close() + + if _, err := buildinfo.Get().WriteTo(os.Stdout); err != nil { + print(fmt.Sprintf("Failed to output build info: %v\n", err)) + } + + if serverPKStr == "" { + // TODO(darkrengarius): fix args passage for Windows + //serverPKStr = "03e9019b3caa021dbee1c23e6295c6034ab4623aec50802fcfdd19764568e2958d" + err := errors.New("VPN server pub key is missing") + print(fmt.Sprintf("%v\n", err)) + setAppErr(appCl, err) + os.Exit(1) + } + + serverPK := cipher.PubKey{} + if err := serverPK.UnmarshalText([]byte(serverPKStr)); err != nil { + print(fmt.Sprintf("Invalid VPN server pub key: %v\n", err)) + setAppErr(appCl, err) + os.Exit(1) + } + + localPK := cipher.PubKey{} + if localPKStr != "" { + if err := localPK.UnmarshalText([]byte(localPKStr)); err != nil { + print(fmt.Sprintf("Invalid local PK: %v\n", err)) + setAppErr(appCl, err) + os.Exit(1) + } + } + + localSK := cipher.SecKey{} + if localSKStr != "" { + if err := localSK.UnmarshalText([]byte(localSKStr)); err != nil { + print(fmt.Sprintf("Invalid local SK: %v\n", err)) + setAppErr(appCl, err) + os.Exit(1) + } + } + + var dnsAddress string + if dnsAddr != "" { + dnsIP := parseIP(dnsAddr) + if dnsIP == nil { + fmt.Println("Invalid DNS Address value. VPN will use current machine DNS.") + dnsAddress = "" + } else { + dnsAddress = dnsIP.String() + } + } + + setAppPort(appCl, appCl.Config().RoutingPort) + + fmt.Printf("Connecting to VPN server %s\n", serverPK.String()) + + vpnClientCfg := vpn.ClientConfig{ + Passcode: passcode, + Killswitch: killswitch, + ServerPK: serverPK, + DNSAddr: dnsAddress, + } + + vpnClient, err := vpn.NewClient(vpnClientCfg, appCl) + if err != nil { + print(fmt.Sprintf("Error creating VPN client: %v\n", err)) + setAppErr(appCl, err) + } + + var directRoutesDone bool + for !directRoutesDone { + select { + case ip := <-directIPsCh: + if err := vpnClient.AddDirectRoute(ip); err != nil { + print(fmt.Sprintf("Failed to setup direct route to %s: %v\n", ip.String(), err)) + setAppErr(appCl, err) + } + default: + directRoutesDone = true + } + } + + go func() { + for ip := range directIPsCh { + if err := vpnClient.AddDirectRoute(ip); err != nil { + print(fmt.Sprintf("Failed to setup direct route to %s: %v\n", ip.String(), err)) + setAppErr(appCl, err) + } + } + }() + + go func() { + for ip := range nonDirectIPsCh { + if err := vpnClient.RemoveDirectRoute(ip); err != nil { + print(fmt.Sprintf("Failed to remove direct route to %s: %v\n", ip.String(), err)) + setAppErr(appCl, err) + } + } + }() + + if runtime.GOOS != "windows" { + osSigs := make(chan os.Signal, 2) + sigs := []os.Signal{syscall.SIGTERM, syscall.SIGINT} + for _, sig := range sigs { + signal.Notify(osSigs, sig) + } + + go func() { + <-osSigs + vpnClient.Close() + }() + } else { + ipcClient, err := ipc.StartClient(visorconfig.VPNClientName, nil) + if err != nil { + print(fmt.Sprintf("Error creating ipc server for VPN client: %v\n", err)) + setAppErr(appCl, err) + os.Exit(1) + } + go vpnClient.ListenIPC(ipcClient) + } + + defer setAppStatus(appCl, appserver.AppDetailedStatusStopped) + + if err := vpnClient.Serve(); err != nil { + print(fmt.Sprintf("Failed to serve VPN: %v\n", err)) + } + }, +} + +func setAppErr(appCl *app.Client, err error) { + if appErr := appCl.SetError(err.Error()); appErr != nil { + print(fmt.Sprintf("Failed to set error %v: %v\n", err, appErr)) + } +} + +func setAppStatus(appCl *app.Client, status appserver.AppDetailedStatus) { + if err := appCl.SetDetailedStatus(string(status)); err != nil { + print(fmt.Sprintf("Failed to set status %v: %v\n", status, err)) + } +} + +func setAppPort(appCl *app.Client, port routing.Port) { + if err := appCl.SetAppPort(port); err != nil { + print(fmt.Sprintf("Failed to set port %v: %v\n", port, err)) + } +} + +// Execute executes root CLI command. +func Execute() { + if err := RootCmd.Execute(); err != nil { + log.Fatal("Failed to execute command: ", err) + } +} diff --git a/cmd/apps/vpn-client/vpn-client.go b/cmd/apps/vpn-client/vpn-client.go index 16a56a598..7320eaa11 100644 --- a/cmd/apps/vpn-client/vpn-client.go +++ b/cmd/apps/vpn-client/vpn-client.go @@ -1,214 +1,43 @@ -// Package main vpn-client.go +// Package main cmd/apps/vpn-client/vpn-client.go package main import ( - "errors" - "flag" - "fmt" - "net" - "os" - "os/signal" - "runtime" - "syscall" + cc "github.com/ivanpirog/coloredcobra" + "github.com/spf13/cobra" - ipc "github.com/james-barrow/golang-ipc" - - "github.com/skycoin/skywire-utilities/pkg/buildinfo" - "github.com/skycoin/skywire-utilities/pkg/cipher" - "github.com/skycoin/skywire/internal/vpn" - "github.com/skycoin/skywire/pkg/app" - "github.com/skycoin/skywire/pkg/app/appevent" - "github.com/skycoin/skywire/pkg/app/appserver" - "github.com/skycoin/skywire/pkg/routing" - "github.com/skycoin/skywire/pkg/visor/visorconfig" + "github.com/skycoin/skywire/cmd/apps/vpn-client/commands" ) -var ( - serverPKStr = flag.String("srv", "", "PubKey of the server to connect to") - localPKStr = flag.String("pk", "", "Local PubKey") - localSKStr = flag.String("sk", "", "Local SecKey") - passcode = flag.String("passcode", "", "Passcode to authenticate connection") - killswitch = flag.Bool("killswitch", false, "If set, the Internet won't be restored during reconnection attempts") - dnsAddr = flag.String("dns", "", "address of DNS want set to tun") -) +func init() { + var helpflag bool + commands.RootCmd.SetUsageTemplate(help) + commands.RootCmd.PersistentFlags().BoolVarP(&helpflag, "help", "h", false, "help menu") + commands.RootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) + commands.RootCmd.PersistentFlags().MarkHidden("help") //nolint +} func main() { - flag.Parse() - - var directIPsCh, nonDirectIPsCh = make(chan net.IP, 100), make(chan net.IP, 100) - defer close(directIPsCh) - defer close(nonDirectIPsCh) - - eventSub := appevent.NewSubscriber() - - parseIP := func(addr string) net.IP { - ip, ok, err := vpn.ParseIP(addr) - if err != nil { - print(fmt.Sprintf("Failed to parse IP %s: %v\n", addr, err)) - return nil - } - if !ok { - print(fmt.Sprintf("Failed to parse IP %s\n", addr)) - return nil - } - - return ip - } - - eventSub.OnTCPDial(func(data appevent.TCPDialData) { - if ip := parseIP(data.RemoteAddr); ip != nil { - directIPsCh <- ip - } + cc.Init(&cc.Config{ + RootCmd: commands.RootCmd, + Headings: cc.HiBlue + cc.Bold, + Commands: cc.HiBlue + cc.Bold, + CmdShortDescr: cc.HiBlue, + Example: cc.HiBlue + cc.Italic, + ExecName: cc.HiBlue + cc.Bold, + Flags: cc.HiBlue + cc.Bold, + FlagsDescr: cc.HiBlue, + NoExtraNewlines: true, + NoBottomNewline: true, }) - - eventSub.OnTCPClose(func(data appevent.TCPCloseData) { - if ip := parseIP(data.RemoteAddr); ip != nil { - nonDirectIPsCh <- ip - } - }) - - appCl := app.NewClient(eventSub) - defer appCl.Close() - - if _, err := buildinfo.Get().WriteTo(os.Stdout); err != nil { - print(fmt.Sprintf("Failed to output build info: %v\n", err)) - } - - if *serverPKStr == "" { - // TODO(darkrengarius): fix args passage for Windows - //*serverPKStr = "03e9019b3caa021dbee1c23e6295c6034ab4623aec50802fcfdd19764568e2958d" - err := errors.New("VPN server pub key is missing") - print(fmt.Sprintf("%v\n", err)) - setAppErr(appCl, err) - os.Exit(1) - } - - serverPK := cipher.PubKey{} - if err := serverPK.UnmarshalText([]byte(*serverPKStr)); err != nil { - print(fmt.Sprintf("Invalid VPN server pub key: %v\n", err)) - setAppErr(appCl, err) - os.Exit(1) - } - - localPK := cipher.PubKey{} - if *localPKStr != "" { - if err := localPK.UnmarshalText([]byte(*localPKStr)); err != nil { - print(fmt.Sprintf("Invalid local PK: %v\n", err)) - setAppErr(appCl, err) - os.Exit(1) - } - } - - localSK := cipher.SecKey{} - if *localSKStr != "" { - if err := localSK.UnmarshalText([]byte(*localSKStr)); err != nil { - print(fmt.Sprintf("Invalid local SK: %v\n", err)) - setAppErr(appCl, err) - os.Exit(1) - } - } - - var dnsAddress string - if *dnsAddr != "" { - dnsIP := parseIP(*dnsAddr) - if dnsIP == nil { - fmt.Println("Invalid DNS Address value. VPN will use current machine DNS.") - dnsAddress = "" - } else { - dnsAddress = dnsIP.String() - } - } - - setAppPort(appCl, appCl.Config().RoutingPort) - - fmt.Printf("Connecting to VPN server %s\n", serverPK.String()) - - vpnClientCfg := vpn.ClientConfig{ - Passcode: *passcode, - Killswitch: *killswitch, - ServerPK: serverPK, - DNSAddr: dnsAddress, - } - - vpnClient, err := vpn.NewClient(vpnClientCfg, appCl) - if err != nil { - print(fmt.Sprintf("Error creating VPN client: %v\n", err)) - setAppErr(appCl, err) - } - - var directRoutesDone bool - for !directRoutesDone { - select { - case ip := <-directIPsCh: - if err := vpnClient.AddDirectRoute(ip); err != nil { - print(fmt.Sprintf("Failed to setup direct route to %s: %v\n", ip.String(), err)) - setAppErr(appCl, err) - } - default: - directRoutesDone = true - } - } - - go func() { - for ip := range directIPsCh { - if err := vpnClient.AddDirectRoute(ip); err != nil { - print(fmt.Sprintf("Failed to setup direct route to %s: %v\n", ip.String(), err)) - setAppErr(appCl, err) - } - } - }() - - go func() { - for ip := range nonDirectIPsCh { - if err := vpnClient.RemoveDirectRoute(ip); err != nil { - print(fmt.Sprintf("Failed to remove direct route to %s: %v\n", ip.String(), err)) - setAppErr(appCl, err) - } - } - }() - - if runtime.GOOS != "windows" { - osSigs := make(chan os.Signal, 2) - sigs := []os.Signal{syscall.SIGTERM, syscall.SIGINT} - for _, sig := range sigs { - signal.Notify(osSigs, sig) - } - - go func() { - <-osSigs - vpnClient.Close() - }() - } else { - ipcClient, err := ipc.StartClient(visorconfig.VPNClientName, nil) - if err != nil { - print(fmt.Sprintf("Error creating ipc server for VPN client: %v\n", err)) - setAppErr(appCl, err) - os.Exit(1) - } - go vpnClient.ListenIPC(ipcClient) - } - - defer setAppStatus(appCl, appserver.AppDetailedStatusStopped) - - if err := vpnClient.Serve(); err != nil { - print(fmt.Sprintf("Failed to serve VPN: %v\n", err)) - } -} - -func setAppErr(appCl *app.Client, err error) { - if appErr := appCl.SetError(err.Error()); appErr != nil { - print(fmt.Sprintf("Failed to set error %v: %v\n", err, appErr)) - } -} - -func setAppStatus(appCl *app.Client, status appserver.AppDetailedStatus) { - if err := appCl.SetDetailedStatus(string(status)); err != nil { - print(fmt.Sprintf("Failed to set status %v: %v\n", status, err)) - } + commands.Execute() } -func setAppPort(appCl *app.Client, port routing.Port) { - if err := appCl.SetAppPort(port); err != nil { - print(fmt.Sprintf("Failed to set port %v: %v\n", port, err)) - } -} +const help = "Usage:\r\n" + + " {{.UseLine}}{{if .HasAvailableSubCommands}}{{end}} {{if gt (len .Aliases) 0}}\r\n\r\n" + + "{{.NameAndAliases}}{{end}}{{if .HasAvailableSubCommands}}\r\n\r\n" + + "Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand)}}\r\n " + + "{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}\r\n\r\n" + + "Flags:\r\n" + + "{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}\r\n\r\n" + + "Global Flags:\r\n" + + "{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}\r\n\r\n" diff --git a/cmd/apps/vpn-server/commands/vpn-server.go b/cmd/apps/vpn-server/commands/vpn-server.go new file mode 100644 index 000000000..685a51a6c --- /dev/null +++ b/cmd/apps/vpn-server/commands/vpn-server.go @@ -0,0 +1,167 @@ +// Package commands vpn-server.go +package commands + +import ( + "errors" + "fmt" + "log" + "os" + "os/signal" + "runtime" + "syscall" + + "github.com/spf13/cobra" + + "github.com/skycoin/skywire-utilities/pkg/buildinfo" + "github.com/skycoin/skywire-utilities/pkg/cipher" + "github.com/skycoin/skywire/internal/vpn" + "github.com/skycoin/skywire/pkg/app" + "github.com/skycoin/skywire/pkg/app/appnet" + "github.com/skycoin/skywire/pkg/app/appserver" + "github.com/skycoin/skywire/pkg/routing" + "github.com/skycoin/skywire/pkg/skyenv" +) + +const ( + netType = appnet.TypeSkynet + vpnPort = routing.Port(skyenv.VPNServerPort) +) + +var ( + localPKStr string + localSKStr string + passcode string + networkIfc string + secure bool +) + +func init() { + RootCmd.Flags().StringVar(&localPKStr, "pk", "", "local pubkey") + RootCmd.Flags().StringVar(&localSKStr, "sk", "", "local seckey") + RootCmd.Flags().StringVar(&passcode, "passcode", "", "passcode to authenticate connecting users") + RootCmd.Flags().StringVar(&networkIfc, "netifc", "", "Default network interface for multiple available interfaces") + RootCmd.Flags().BoolVar(&secure, "secure", true, "Forbid connections from clients to server local network") +} + +// RootCmd is the root command for skywire-cli +var RootCmd = &cobra.Command{ + Use: "vpn-server", + Short: "skywire vpn server application", + Long: ` + ┬ ┬┌─┐┌┐┌ ┌─┐┌─┐┬─┐┬ ┬┌─┐┬─┐ + └┐┌┘├─┘│││───└─┐├┤ ├┬┘└┐┌┘├┤ ├┬┘ + └┘ ┴ ┘└┘ └─┘└─┘┴└─ └┘ └─┘┴└─`, + SilenceErrors: true, + SilenceUsage: true, + DisableSuggestions: true, + DisableFlagsInUseLine: true, + Version: buildinfo.Version(), + Run: func(cmd *cobra.Command, args []string) { + appCl := app.NewClient(nil) + defer appCl.Close() + + if _, err := buildinfo.Get().WriteTo(os.Stdout); err != nil { + print(fmt.Sprintf("Failed to output build info: %v\n", err)) + } + + if runtime.GOOS != "linux" { + err := errors.New("OS is not supported") + print(err) + setAppErr(appCl, err) + os.Exit(1) + } + + localPK := cipher.PubKey{} + if localPKStr != "" { + if err := localPK.UnmarshalText([]byte(localPKStr)); err != nil { + print(fmt.Sprintf("Invalid local PK: %v\n", err)) + setAppErr(appCl, err) + os.Exit(1) + } + } + + localSK := cipher.SecKey{} + if localSKStr != "" { + if err := localSK.UnmarshalText([]byte(localSKStr)); err != nil { + print(fmt.Sprintf("Invalid local SK: %v\n", err)) + setAppErr(appCl, err) + os.Exit(1) + } + } + + osSigs := make(chan os.Signal, 2) + + sigs := []os.Signal{syscall.SIGTERM, syscall.SIGINT} + for _, sig := range sigs { + signal.Notify(osSigs, sig) + } + + l, err := appCl.Listen(netType, vpnPort) + if err != nil { + print(fmt.Sprintf("Error listening network %v on port %d: %v\n", netType, vpnPort, err)) + setAppErr(appCl, err) + os.Exit(1) + } + setAppPort(appCl, vpnPort) + fmt.Printf("Got app listener, bound to %d\n", vpnPort) + + srvCfg := vpn.ServerConfig{ + Passcode: passcode, + Secure: secure, + NetworkInterface: networkIfc, + } + srv, err := vpn.NewServer(srvCfg, appCl) + if err != nil { + print(fmt.Sprintf("Error creating VPN server: %v\n", err)) + setAppErr(appCl, err) + os.Exit(1) + } + defer func() { + if err := srv.Close(); err != nil { + print(fmt.Sprintf("Error closing server: %v\n", err)) + } + }() + + errCh := make(chan error) + go func() { + if err := srv.Serve(l); err != nil { + errCh <- err + } + + close(errCh) + }() + + defer setAppStatus(appCl, appserver.AppDetailedStatusStopped) + + select { + case <-osSigs: + case err := <-errCh: + print(fmt.Sprintf("Error serving: %v\n", err)) + } + }, +} + +func setAppErr(appCl *app.Client, err error) { + if appErr := appCl.SetError(err.Error()); appErr != nil { + print(fmt.Sprintf("Failed to set error %v: %v\n", err, appErr)) + } +} + +func setAppStatus(appCl *app.Client, status appserver.AppDetailedStatus) { + if err := appCl.SetDetailedStatus(string(status)); err != nil { + print(fmt.Sprintf("Failed to set status %v: %v\n", status, err)) + } +} + +func setAppPort(appCl *app.Client, port routing.Port) { + if err := appCl.SetAppPort(port); err != nil { + print(fmt.Sprintf("Failed to set port %v: %v\n", port, err)) + } +} + +// Execute executes root CLI command. +func Execute() { + if err := RootCmd.Execute(); err != nil { + log.Fatal("Failed to execute command: ", err) + } +} diff --git a/cmd/apps/vpn-server/vpn-server.go b/cmd/apps/vpn-server/vpn-server.go index 8b56224b8..07175778c 100644 --- a/cmd/apps/vpn-server/vpn-server.go +++ b/cmd/apps/vpn-server/vpn-server.go @@ -1,139 +1,43 @@ -// Package main vpn-server.go +// Package main cmd/apps/vpn-server/vpn-server.go package main import ( - "errors" - "flag" - "fmt" - "os" - "os/signal" - "runtime" - "syscall" + cc "github.com/ivanpirog/coloredcobra" + "github.com/spf13/cobra" - "github.com/skycoin/skywire-utilities/pkg/buildinfo" - "github.com/skycoin/skywire-utilities/pkg/cipher" - "github.com/skycoin/skywire/internal/vpn" - "github.com/skycoin/skywire/pkg/app" - "github.com/skycoin/skywire/pkg/app/appnet" - "github.com/skycoin/skywire/pkg/app/appserver" - "github.com/skycoin/skywire/pkg/routing" - "github.com/skycoin/skywire/pkg/skyenv" + "github.com/skycoin/skywire/cmd/apps/vpn-server/commands" ) -const ( - netType = appnet.TypeSkynet - vpnPort = routing.Port(skyenv.VPNServerPort) -) - -var ( - localPKStr = flag.String("pk", "", "Local PubKey") - localSKStr = flag.String("sk", "", "Local SecKey") - passcode = flag.String("passcode", "", "Passcode to authenticate connecting users") - networkIfc = flag.String("netifc", "", "Default network interface for multiple available interfaces") - secure = flag.Bool("secure", true, "Forbid connections from clients to server local network") -) - -func main() { - - appCl := app.NewClient(nil) - defer appCl.Close() - - if _, err := buildinfo.Get().WriteTo(os.Stdout); err != nil { - print(fmt.Sprintf("Failed to output build info: %v\n", err)) - } - - if runtime.GOOS != "linux" { - err := errors.New("OS is not supported") - print(err) - setAppErr(appCl, err) - os.Exit(1) - } - - flag.Parse() - - localPK := cipher.PubKey{} - if *localPKStr != "" { - if err := localPK.UnmarshalText([]byte(*localPKStr)); err != nil { - print(fmt.Sprintf("Invalid local PK: %v\n", err)) - setAppErr(appCl, err) - os.Exit(1) - } - } - - localSK := cipher.SecKey{} - if *localSKStr != "" { - if err := localSK.UnmarshalText([]byte(*localSKStr)); err != nil { - print(fmt.Sprintf("Invalid local SK: %v\n", err)) - setAppErr(appCl, err) - os.Exit(1) - } - } - - osSigs := make(chan os.Signal, 2) - - sigs := []os.Signal{syscall.SIGTERM, syscall.SIGINT} - for _, sig := range sigs { - signal.Notify(osSigs, sig) - } - - l, err := appCl.Listen(netType, vpnPort) - if err != nil { - print(fmt.Sprintf("Error listening network %v on port %d: %v\n", netType, vpnPort, err)) - setAppErr(appCl, err) - os.Exit(1) - } - setAppPort(appCl, vpnPort) - fmt.Printf("Got app listener, bound to %d\n", vpnPort) - - srvCfg := vpn.ServerConfig{ - Passcode: *passcode, - Secure: *secure, - NetworkInterface: *networkIfc, - } - srv, err := vpn.NewServer(srvCfg, appCl) - if err != nil { - print(fmt.Sprintf("Error creating VPN server: %v\n", err)) - setAppErr(appCl, err) - os.Exit(1) - } - defer func() { - if err := srv.Close(); err != nil { - print(fmt.Sprintf("Error closing server: %v\n", err)) - } - }() - - errCh := make(chan error) - go func() { - if err := srv.Serve(l); err != nil { - errCh <- err - } - - close(errCh) - }() - - defer setAppStatus(appCl, appserver.AppDetailedStatusStopped) - - select { - case <-osSigs: - case err := <-errCh: - print(fmt.Sprintf("Error serving: %v\n", err)) - } -} - -func setAppErr(appCl *app.Client, err error) { - if appErr := appCl.SetError(err.Error()); appErr != nil { - print(fmt.Sprintf("Failed to set error %v: %v\n", err, appErr)) - } +func init() { + var helpflag bool + commands.RootCmd.SetUsageTemplate(help) + commands.RootCmd.PersistentFlags().BoolVarP(&helpflag, "help", "h", false, "help menu") + commands.RootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) + commands.RootCmd.PersistentFlags().MarkHidden("help") //nolint } -func setAppStatus(appCl *app.Client, status appserver.AppDetailedStatus) { - if err := appCl.SetDetailedStatus(string(status)); err != nil { - print(fmt.Sprintf("Failed to set status %v: %v\n", status, err)) - } +func main() { + cc.Init(&cc.Config{ + RootCmd: commands.RootCmd, + Headings: cc.HiBlue + cc.Bold, + Commands: cc.HiBlue + cc.Bold, + CmdShortDescr: cc.HiBlue, + Example: cc.HiBlue + cc.Italic, + ExecName: cc.HiBlue + cc.Bold, + Flags: cc.HiBlue + cc.Bold, + FlagsDescr: cc.HiBlue, + NoExtraNewlines: true, + NoBottomNewline: true, + }) + commands.Execute() } -func setAppPort(appCl *app.Client, port routing.Port) { - if err := appCl.SetAppPort(port); err != nil { - print(fmt.Sprintf("Failed to set port %v: %v\n", port, err)) - } -} +const help = "Usage:\r\n" + + " {{.UseLine}}{{if .HasAvailableSubCommands}}{{end}} {{if gt (len .Aliases) 0}}\r\n\r\n" + + "{{.NameAndAliases}}{{end}}{{if .HasAvailableSubCommands}}\r\n\r\n" + + "Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand)}}\r\n " + + "{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}\r\n\r\n" + + "Flags:\r\n" + + "{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}\r\n\r\n" + + "Global Flags:\r\n" + + "{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}\r\n\r\n" diff --git a/cmd/setup-node/commands/root.go b/cmd/setup-node/commands/root.go index b41f654b8..ddc170d1e 100644 --- a/cmd/setup-node/commands/root.go +++ b/cmd/setup-node/commands/root.go @@ -5,10 +5,13 @@ import ( "bufio" "context" "encoding/json" + "fmt" "io" + "log" "os" + "path/filepath" + "strings" - cc "github.com/ivanpirog/coloredcobra" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -18,26 +21,25 @@ import ( "github.com/skycoin/skywire-utilities/pkg/metricsutil" "github.com/skycoin/skywire/pkg/router" "github.com/skycoin/skywire/pkg/router/setupmetrics" - "github.com/skycoin/skywire/pkg/syslog" ) var ( metricsAddr string - syslogAddr string tag string cfgFromStdin bool ) func init() { RootCmd.Flags().StringVarP(&metricsAddr, "metrics", "m", "", "address to bind metrics API to") - RootCmd.Flags().StringVar(&syslogAddr, "syslog", "", "syslog server address. E.g. localhost:514") RootCmd.Flags().StringVar(&tag, "tag", "setup_node", "logging tag") RootCmd.Flags().BoolVarP(&cfgFromStdin, "stdin", "i", false, "read config from STDIN") } // RootCmd is the root command for setup node var RootCmd = &cobra.Command{ - Use: "setup-node [config.json]", + Use: func() string { + return strings.Split(filepath.Base(strings.ReplaceAll(strings.ReplaceAll(fmt.Sprintf("%v", os.Args), "[", ""), "]", ""))+" [config.json]", " ")[0] + }(), Short: "Route Setup Node for skywire", Long: ` ┌─┐┌─┐┌┬┐┬ ┬┌─┐ ┌┐┌┌─┐┌┬┐┌─┐ @@ -52,15 +54,6 @@ var RootCmd = &cobra.Command{ mLog.Printf("Failed to output build info: %v", err) } - if syslogAddr != "" { - hook, err := syslog.SetupHook(syslogAddr, tag) - if err != nil { - log.Fatalf("Error setting up syslog: %v", err) - } - - logging.AddHook(hook) - } - var rdr io.Reader var err error @@ -124,20 +117,7 @@ func prepareMetrics(log logrus.FieldLogger) setupmetrics.Metrics { // Execute executes root CLI command. func Execute() { - cc.Init(&cc.Config{ - RootCmd: RootCmd, - Headings: cc.HiBlue + cc.Bold, - Commands: cc.HiBlue + cc.Bold, - CmdShortDescr: cc.HiBlue, - Example: cc.HiBlue + cc.Italic, - ExecName: cc.HiBlue + cc.Bold, - Flags: cc.HiBlue + cc.Bold, - FlagsDataType: cc.HiBlue, - FlagsDescr: cc.HiBlue, - NoExtraNewlines: true, - NoBottomNewline: true, - }) if err := RootCmd.Execute(); err != nil { - panic(err) + log.Fatal("Failed to execute command: ", err) } } diff --git a/cmd/setup-node/setup-node.go b/cmd/setup-node/setup-node.go index befbef0ff..9e8ef763a 100644 --- a/cmd/setup-node/setup-node.go +++ b/cmd/setup-node/setup-node.go @@ -3,9 +3,42 @@ package main import ( + cc "github.com/ivanpirog/coloredcobra" + "github.com/spf13/cobra" + "github.com/skycoin/skywire/cmd/setup-node/commands" ) +func init() { + var helpflag bool + commands.RootCmd.SetUsageTemplate(help) + commands.RootCmd.PersistentFlags().BoolVarP(&helpflag, "help", "h", false, "help menu") + commands.RootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) + commands.RootCmd.PersistentFlags().MarkHidden("help") //nolint +} + func main() { + cc.Init(&cc.Config{ + RootCmd: commands.RootCmd, + Headings: cc.HiBlue + cc.Bold, + Commands: cc.HiBlue + cc.Bold, + CmdShortDescr: cc.HiBlue, + Example: cc.HiBlue + cc.Italic, + ExecName: cc.HiBlue + cc.Bold, + Flags: cc.HiBlue + cc.Bold, + FlagsDescr: cc.HiBlue, + NoExtraNewlines: true, + NoBottomNewline: true, + }) commands.Execute() } + +const help = "Usage:\r\n" + + " {{.UseLine}}{{if .HasAvailableSubCommands}}{{end}} {{if gt (len .Aliases) 0}}\r\n\r\n" + + "{{.NameAndAliases}}{{end}}{{if .HasAvailableSubCommands}}\r\n\r\n" + + "Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand)}}\r\n " + + "{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}\r\n\r\n" + + "Flags:\r\n" + + "{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}\r\n\r\n" + + "Global Flags:\r\n" + + "{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}\r\n\r\n" diff --git a/cmd/skywire-cli/README.md b/cmd/skywire-cli/README.md index d5409dfed..4ae5a12a3 100644 --- a/cmd/skywire-cli/README.md +++ b/cmd/skywire-cli/README.md @@ -124,6 +124,7 @@ Available Commands: tree subcommand tree doc generate markdown docs dmsghttp update dmsghttp-config.json file from config bootstrap service + services update services-config.json file from config service ``` @@ -2190,3 +2191,17 @@ Flags: ``` + +### services + +``` +update services-config.json file from config service + +Usage: + cli services update [flags] + +Flags: + -p, --path string path of services-config file, default is for pkg installation (default "/opt/skywire/services-config.json") + + +``` diff --git a/cmd/skywire-cli/commands/dmsghttp/root.go b/cmd/skywire-cli/commands/config/dmsghttp.go similarity index 92% rename from cmd/skywire-cli/commands/dmsghttp/root.go rename to cmd/skywire-cli/commands/config/dmsghttp.go index 504e715ea..17bd2c698 100644 --- a/cmd/skywire-cli/commands/dmsghttp/root.go +++ b/cmd/skywire-cli/commands/config/dmsghttp.go @@ -1,5 +1,5 @@ -// Package clidmsghttp cmd/skywire-cli/commands/dmsghttp/root.go -package clidmsghttp +// Package cliconfig cmd/skywire-cli/commands/config/dmsghttp.go +package cliconfig import ( "context" @@ -16,20 +16,15 @@ import ( "github.com/skycoin/skywire-utilities/pkg/skyenv" ) -var ( - path string -) - func init() { + updateCmd.AddCommand(dmsghttpCmd) dmsghttpCmd.Flags().SortFlags = false + //TODO: fix path for non linux package defaults dmsghttpCmd.Flags().StringVarP(&path, "path", "p", "/opt/skywire/dmsghttp-config.json", "path of dmsghttp-config file, default is for pkg installation") } -// RootCmd is surveyCmd -var RootCmd = dmsghttpCmd - var dmsghttpCmd = &cobra.Command{ - Use: "dmsghttp update", + Use: "dmsghttp", Short: "update dmsghttp-config.json file from config bootstrap service", Run: func(cmd *cobra.Command, args []string) { log := logging.MustGetLogger("dmsghttp_updater") diff --git a/cmd/skywire-cli/commands/config/gen.go b/cmd/skywire-cli/commands/config/gen.go index d986a44cb..4eeb8436b 100644 --- a/cmd/skywire-cli/commands/config/gen.go +++ b/cmd/skywire-cli/commands/config/gen.go @@ -10,7 +10,6 @@ import ( "os" "os/exec" "path/filepath" - "strconv" "strings" "time" @@ -82,7 +81,9 @@ func init() { gHiddenFlags = append(gHiddenFlags, "noauth") genConfigCmd.Flags().BoolVarP(&isDmsgHTTP, "dmsghttp", "d", scriptExecBool("${DMSGHTTP:-false}"), "use dmsg connection to skywire services\033[0m") gHiddenFlags = append(gHiddenFlags, "dmsghttp") - genConfigCmd.Flags().IntVar(&minDmsgSess, "minsess", scriptExecInt("${MINDMSGSESS:-1}"), "number of dmsg servers to connect to (0 = unlimited)\033[0m") + genConfigCmd.Flags().StringVarP(&dmsgHTTPPath, "dmsgconf", "D", scriptExecString(fmt.Sprintf("${DMSGCONF:-%s}", visorconfig.DMSGHTTPName)), "dmsghttp-config path\033[0m") + gHiddenFlags = append(gHiddenFlags, "dmsgconf") + genConfigCmd.Flags().IntVar(&minDmsgSess, "minsess", scriptExecInt("${MINDMSGSESS:-2}"), "number of dmsg servers to connect to (0 = unlimited)\033[0m") gHiddenFlags = append(gHiddenFlags, "minsess") genConfigCmd.Flags().BoolVarP(&isEnableAuth, "auth", "e", false, "enable auth on hypervisor UI\033[0m") gHiddenFlags = append(gHiddenFlags, "auth") @@ -129,6 +130,8 @@ func init() { gHiddenFlags = append(gHiddenFlags, "example-apps") genConfigCmd.Flags().BoolVarP(&isStdout, "stdout", "n", false, "write config to stdout\033[0m") gHiddenFlags = append(gHiddenFlags, "stdout") + genConfigCmd.Flags().BoolVarP(&isSquash, "squash", "N", false, "output config without whitespace or newlines\033[0m") + gHiddenFlags = append(gHiddenFlags, "squash") genConfigCmd.Flags().BoolVarP(&isEnvs, "envs", "q", false, "show the environmental variable settings") msg = "output config" if scriptExecString("${OUTPUT}") == "" { @@ -172,7 +175,7 @@ func init() { gHiddenFlags = append(gHiddenFlags, "stcpr") genConfigCmd.Flags().IntVar(&sudphPort, "sudph", scriptExecInt("${SUDPHPORT:-0}"), "set udp transport listening port - 0 for random\033[0m") gHiddenFlags = append(gHiddenFlags, "sudph") - genConfigCmd.Flags().StringVar(&binPath, "binpath", scriptExecString("${BINPATH}"), "set bin_path\033[0m") + genConfigCmd.Flags().StringVar(&binPath, "binpath", scriptExecString("${BINPATH}"), "set bin_path for visor vative apps\033[0m") gHiddenFlags = append(gHiddenFlags, "binpath") genConfigCmd.Flags().StringVar(&addSkysocksClientSrv, "proxyclientpk", scriptExecString("${PROXYCLIENTPK}"), "set server public key for proxy client") gHiddenFlags = append(gHiddenFlags, "proxyclientpk") @@ -185,6 +188,7 @@ func init() { genConfigCmd.Flags().StringVar(&proxyClientPass, "proxyclientpass", scriptExecString("${PROXYCLIENTPASS}"), "password for the proxy client to access the server (if needed)") gHiddenFlags = append(gHiddenFlags, "proxyclientpass") // TODO: Password for accessing proxy client + // TODO: VPN client killswitch should be handled as boolean, not string genConfigCmd.Flags().StringVar(&setVPNClientKillswitch, "killsw", scriptExecString("${VPNKS}"), "vpn client killswitch") gHiddenFlags = append(gHiddenFlags, "killsw") genConfigCmd.Flags().StringVar(&addVPNClientSrv, "addvpn", scriptExecString("${ADDVPNPK}"), "set vpn server public key for vpn client") @@ -199,10 +203,13 @@ func init() { gHiddenFlags = append(gHiddenFlags, "netifc") genConfigCmd.Flags().BoolVar(&noFetch, "nofetch", false, "do not fetch the services from the service conf url") gHiddenFlags = append(gHiddenFlags, "nofetch") - genConfigCmd.Flags().StringVar(&configServicePath, "confpath", "", "specify service conf file (instead of fetching from URL)") - gHiddenFlags = append(gHiddenFlags, "confpath") + //TODO: visorconfig.SvcConfName + genConfigCmd.Flags().StringVarP(&configServicePath, "svcconf", "S", scriptExecString(fmt.Sprintf("${SVCCONF:-%s}", visorconfig.SERVICESName)), "fallback service configuration file\033[0m") + gHiddenFlags = append(gHiddenFlags, "svcconf") genConfigCmd.Flags().BoolVar(&noDefaults, "nodefaults", false, "do not use hardcoded defaults for production / test services") gHiddenFlags = append(gHiddenFlags, "nodefaults") + genConfigCmd.Flags().BoolVar(&snConfig, "sn", false, "generate config for route setup-node") + gHiddenFlags = append(gHiddenFlags, "sn") genConfigCmd.Flags().StringVar(&ver, "version", scriptExecString("${VERSION}"), "custom version testing override\033[0m") gHiddenFlags = append(gHiddenFlags, "version") genConfigCmd.Flags().BoolVar(&isAll, "all", false, "show all flags") @@ -215,122 +222,6 @@ func init() { } } -func scriptExecString(s string) string { - if visorconfig.OS == "windows" { - var variable, defaultvalue string - if strings.Contains(s, ":-") { - parts := strings.SplitN(s, ":-", 2) - variable = parts[0] + "}" - defaultvalue = strings.TrimRight(parts[1], "}") - } else { - variable = s - defaultvalue = "" - } - out, err := script.Exec(fmt.Sprintf(`powershell -c '$SKYENV = "%s"; if ($SKYENV -ne "" -and (Test-Path $SKYENV)) { . $SKYENV }; echo %s"`, skyenvfile, variable)).String() - if err == nil { - if (out == "") || (out == variable) { - return defaultvalue - } - return strings.TrimRight(out, "\n") - } - return defaultvalue - } - z, err := script.Exec(fmt.Sprintf(`bash -c 'SKYENV=%s ; if [[ $SKYENV != "" ]] && [[ -f $SKYENV ]] ; then source $SKYENV ; fi ; printf "%s"'`, skyenvfile, s)).String() - if err == nil { - return strings.TrimSpace(z) - } - return "" -} - -func scriptExecBool(s string) bool { - if visorconfig.OS == "windows" { - var variable string - if strings.Contains(s, ":-") { - parts := strings.SplitN(s, ":-", 2) - variable = parts[0] + "}" - } else { - variable = s - } - out, err := script.Exec(fmt.Sprintf(`powershell -c '$SKYENV = "%s"; if ($SKYENV -ne "" -and (Test-Path $SKYENV)) { . $SKYENV }; echo %s"`, skyenvfile, variable)).String() - if err == nil { - if (out == "") || (out == variable) { - return false - } - b, err := strconv.ParseBool(strings.TrimSpace(strings.TrimRight(out, "\n"))) - if err == nil { - return b - } - } - return false - } - z, err := script.Exec(fmt.Sprintf(`bash -c 'SKYENV=%s ; if [[ $SKYENV != "" ]] && [[ -f $SKYENV ]] ; then source $SKYENV ; fi ; printf "%s"'`, skyenvfile, s)).String() - if err == nil { - b, err := strconv.ParseBool(z) - if err == nil { - return b - } - } - - return false -} - -func scriptExecArray(s string) string { - if visorconfig.OS == "windows" { - variable := s - if strings.Contains(variable, "[@]}") { - variable = strings.TrimRight(variable, "[@]}") - variable = strings.TrimRight(variable, "{") - } - out, err := script.Exec(fmt.Sprintf(`powershell -c '$SKYENV = "%s"; if ($SKYENV -ne "" -and (Test-Path $SKYENV)) { . $SKYENV }; foreach ($item in %s) { Write-Host $item }'`, skyenvfile, variable)).Slice() - if err == nil { - if len(out) != 0 { - return "" - } - return strings.Join(out, ",") - } - } - y, err := script.Exec(fmt.Sprintf(`bash -c 'SKYENV=%s ; if [[ $SKYENV != "" ]] && [[ -f $SKYENV ]] ; then source $SKYENV ; fi ; for _i in %s ; do echo "$_i" ; done'`, skyenvfile, s)).Slice() - if err == nil { - return strings.Join(y, ",") - } - return "" -} - -func scriptExecInt(s string) int { - if visorconfig.OS == "windows" { - var variable string - if strings.Contains(s, ":-") { - parts := strings.SplitN(s, ":-", 2) - variable = parts[0] + "}" - } else { - variable = s - } - out, err := script.Exec(fmt.Sprintf(`powershell -c '$SKYENV = "%s"; if ($SKYENV -ne "" -and (Test-Path $SKYENV)) { . $SKYENV }; echo %s"`, skyenvfile, variable)).String() - if err == nil { - if (out == "") || (out == variable) { - return 0 - } - i, err := strconv.Atoi(strings.TrimSpace(strings.TrimRight(out, "\n"))) - if err == nil { - return i - } - return 0 - } - return 0 - } - z, err := script.Exec(fmt.Sprintf(`bash -c 'SKYENV=%s ; if [[ $SKYENV != "" ]] && [[ -f $SKYENV ]] ; then source $SKYENV ; fi ; printf "%s"'`, skyenvfile, s)).String() - if err == nil { - if z == "" { - return 0 - } - i, err := strconv.Atoi(z) - if err == nil { - return i - } - } - return 0 -} - var genConfigCmd = &cobra.Command{ Use: "gen", Short: "Generate a config file", @@ -339,8 +230,10 @@ var genConfigCmd = &cobra.Command{ if skyenvfile == "" { return `Generate a config file - Config defaults file may also be specified with - SKYENV=/path/to/skywire.conf skywire-cli config gen` + Config defaults file may also be specified with: + SKYENV=/path/to/skywire.conf skywire-cli config gen + print the SKYENV file template with: + skywire-cli config gen -q` } if _, err := os.Stat(skyenvfile); err == nil { return `Generate a config file @@ -350,7 +243,9 @@ var genConfigCmd = &cobra.Command{ return `Generate a config file Config defaults file may also be specified with - SKYENV=/path/to/skywire.conf skywire-cli config gen` + SKYENV=/path/to/skywire.conf skywire-cli config gen + print the SKYENV file template with: + skywire-cli config gen -q` } return `Generate a config file` @@ -405,11 +300,16 @@ var genConfigCmd = &cobra.Command{ if isUsrEnv { isHypervisor = true } + //use test deployment + if isTestEnv { + serviceConfURL = utilenv.TestServiceConfAddr + } var err error if isDmsgHTTP { - dmsgHTTPPath := visorconfig.DMSGHTTPName if isPkgEnv { - dmsgHTTPPath = visorconfig.SkywirePath + "/" + visorconfig.DMSGHTTPName + if dmsgHTTPPath == visorconfig.DMSGHTTPName { + dmsgHTTPPath = visorconfig.SkywirePath + "/" + visorconfig.DMSGHTTPName //nolint + } } if _, err := os.Stat(dmsgHTTPPath); err == nil { if !isStdout { @@ -483,13 +383,33 @@ var genConfigCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { log := logger + wasStdout := isStdout + var body []byte + var err error + // enable errors from service conf fetch from the combination of these flags + if isStdout && isHide { + isStdout = false + } if !noFetch { - wasStdout := isStdout - var body []byte - var err error - - if configServicePath != "" { + // create an http client to fetch the services + client := http.Client{ + Timeout: time.Second * 15, // Timeout after 15 seconds + } + if serviceConfURL == "" { + serviceConfURL = utilenv.ServiceConfAddr + } + if !isStdout { + log.Infof("Fetching service endpoints from %s", serviceConfURL) + } + // Make the HTTP GET request + res, err := client.Get(fmt.Sprint(serviceConfURL)) + if err != nil { + //silence errors for stdout + if !isStdout { + log.WithError(err).Error("Failed to fetch servers\n") + log.Warn("Falling back on services-config.json") + } body, err = os.ReadFile(configServicePath) if err != nil { if !isStdout { @@ -498,66 +418,72 @@ var genConfigCmd = &cobra.Command{ } } else { //fill in services struct with the response - err = json.Unmarshal(body, &services) + err = json.Unmarshal(body, &servicesConfig) if err != nil { - log.WithError(err).Fatal("Failed to unmarshal json response\n") - } - if !isStdout { - log.Infof("Fetched service endpoints from '%s'", serviceConfURL) + if !isStdout { + log.WithError(err).Error("Failed to unmarshal services-config.json file\n") + } + } else { + services = servicesConfig.Prod + if isTestEnv { + services = servicesConfig.Test + } } - // reset the state of isStdout - isStdout = wasStdout } } else { - // set default service conf url if none is specified - if serviceConfURL == "" { - serviceConfURL = utilenv.ServiceConfAddr - } - //use test deployment - if isTestEnv { - serviceConfURL = utilenv.TestServiceConfAddr - } - // enable errors from service conf fetch from the combination of these flags - - if isStdout && isHide { - isStdout = false + //nil error on service conf fetch + if res.Body != nil { + defer res.Body.Close() //nolint } - // create an http client to fetch the services - client := http.Client{ - Timeout: time.Second * 15, // Timeout after 15 seconds + body, err = io.ReadAll(res.Body) + if err != nil { + log.WithError(err).Error("Failed to read http response\n") } - // Make the HTTP GET request - res, err := client.Get(fmt.Sprint(serviceConfURL)) + //fill in services struct with the response + err = json.Unmarshal(body, &services) if err != nil { - //silence errors for stdout if !isStdout { - log.WithError(err).Error("Failed to fetch servers\n") + log.WithError(err).Error("Failed to unmarshal json response to services struct\n") log.Warn("Falling back on hardcoded servers") } } else { - if res.Body != nil { - defer res.Body.Close() //nolint - } - body, err = io.ReadAll(res.Body) - if err != nil { - log.WithError(err).Fatal("Failed to read response\n") + if !isStdout { + log.Infof("Fetched service endpoints from '%s'", serviceConfURL) } } + } + } else { + if isPkgEnv { + if configServicePath == visorconfig.SERVICESName { + configServicePath = visorconfig.SkywirePath + "/" + visorconfig.SERVICESName + } + } + body, err = os.ReadFile(configServicePath) + if err != nil { + if !isStdout { + log.WithError(err).Error("Failed to read config service from file\n") + log.Warn("Falling back on hardcoded servers") + } + } else { //fill in services struct with the response - err = json.Unmarshal(body, &services) + err = json.Unmarshal(body, &servicesConfig) if err != nil { - log.WithError(err).Fatal("Failed to unmarshal json response\n") + if !isStdout { + log.WithError(err).Error("Failed to unmarshal json response to services struct\n") + log.Warn("Falling back on hardcoded servers") + } } - if !isStdout { - log.Infof("Fetched service endpoints from '%s'", serviceConfURL) + services = servicesConfig.Prod + if isTestEnv { + services = servicesConfig.Test } - - // reset the state of isStdout - isStdout = wasStdout } + } + // reset the state of isStdout + isStdout = wasStdout // Read in old config and obtain old secret key or generate a new random secret key // and obtain old hypervisors (if any) var oldConf visorconfig.V1 @@ -627,17 +553,16 @@ var genConfigCmd = &cobra.Command{ } if isDmsgHTTP { - dmsghttpConfig := visorconfig.DMSGHTTPName // TODO //if isUsrEnv { - // dmsghttpConfig = homepath + "/" + visorconfig.DMSGHTTPName + // dmsgHTTPPath = homepath + "/" + visorconfig.DMSGHTTPName //} if isPkgEnv { - dmsghttpConfig = visorconfig.SkywirePath + "/" + visorconfig.DMSGHTTPName + dmsgHTTPPath = visorconfig.SkywirePath + "/" + visorconfig.DMSGHTTPName //nolint } // Read the JSON configuration file - dmsghttpConfigData, err := os.ReadFile(dmsghttpConfig) //nolint + dmsghttpConfigData, err := os.ReadFile(dmsgHTTPPath) //nolint if err != nil { log.Fatalf("Failed to read config file: %v", err) } @@ -778,9 +703,10 @@ var genConfigCmd = &cobra.Command{ } conf.Dmsg = &dmsgc.DmsgConfig{ - Discovery: services.DmsgDiscovery, - SessionsCount: minDmsgSess, - Servers: []*disc.Entry{}, + Discovery: services.DmsgDiscovery, + SessionsCount: minDmsgSess, + Servers: []*disc.Entry{}, + ConnectedServersType: "all", } conf.Transport = &visorconfig.Transport{ Discovery: services.TransportDiscovery, //utilenv.TpDiscAddr, @@ -924,84 +850,41 @@ var genConfigCmd = &cobra.Command{ conf.Launcher.Apps = []appserver.AppConfig{ { Name: visorconfig.VPNClientName, - Binary: visorconfig.VPNClientName, + Binary: "skywire", AutoStart: false, Port: routing.Port(skyenv.VPNClientPort), - Args: []string{"-dns", dnsServer}, + Args: []string{"app", "vpn-client", "--dns", dnsServer}, }, { Name: visorconfig.SkychatName, - Binary: visorconfig.SkychatName, + Binary: "skywire", AutoStart: true, Port: routing.Port(skyenv.SkychatPort), - Args: []string{"-addr", visorconfig.SkychatAddr}, + Args: []string{"app", "skychat", "--addr", visorconfig.SkychatAddr}, }, { Name: visorconfig.SkysocksName, - Binary: visorconfig.SkysocksName, + Binary: "skywire", AutoStart: true, Port: routing.Port(visorconfig.SkysocksPort), + Args: []string{"app", "skysocks"}, }, { Name: visorconfig.SkysocksClientName, - Binary: visorconfig.SkysocksClientName, + Binary: "skywire", AutoStart: false, Port: routing.Port(visorconfig.SkysocksClientPort), - Args: []string{"-addr", visorconfig.SkysocksClientAddr}, + Args: []string{"app", "skysocks-client", "--addr", visorconfig.SkysocksClientAddr}, }, { Name: visorconfig.VPNServerName, - Binary: visorconfig.VPNServerName, + Binary: "skywire", AutoStart: isVpnServerEnable, + Args: []string{"app", "vpn-server"}, Port: routing.Port(visorconfig.VPNServerPort), }, } - skywire := os.Args[0] - isMatch := strings.Contains("/tmp/", skywire) - if (!isStdout) || (!isMatch) { - //binaries have .exe extension on windows - var exe string - if visorconfig.OS == "win" { - exe = ".exe" - } - // Disable apps not found at bin_path with above exceptions for go run and stdout - if _, err := os.Stat(conf.Launcher.BinPath + "/" + "skychat" + exe); err != nil { - if disableApps == "" { - disableApps = "skychat" - } else { - disableApps = disableApps + ",skychat" - } - } - if _, err := os.Stat(conf.Launcher.BinPath + "/" + "skysocks" + exe); err != nil { - if disableApps == "" { - disableApps = "skysocks" - } else { - disableApps = disableApps + ",skysocks" - } - } - if _, err := os.Stat(conf.Launcher.BinPath + "/" + "skysocks-client" + exe); err != nil { - if disableApps == "" { - disableApps = "skysocks-client" - } else { - disableApps = disableApps + ",skysocks-client" - } - } - if _, err := os.Stat(conf.Launcher.BinPath + "/" + "vpn-client" + exe); err != nil { - if disableApps == "" { - disableApps = "vpn-client" - } else { - disableApps = disableApps + ",vpn-client" - } - } - if _, err := os.Stat(conf.Launcher.BinPath + "/" + "vpn-server" + exe); err != nil { - if disableApps == "" { - disableApps = "vpn-server" - } else { - disableApps = disableApps + ",vpn-server" - } - } - } // Disable apps --disable-apps flag if disableApps != "" { apps := strings.Split(disableApps, ",") @@ -1140,7 +1023,13 @@ var genConfigCmd = &cobra.Command{ // Marshal the modified config to JSON with indentation jsonData, err := json.MarshalIndent(conf, "", " ") if err != nil { - log.Fatalf("Failed to marshal config to JSON: %v", err) + log.WithError(err).Fatal("Failed to marshal config to indented JSON") + } + if snConfig { + jsonData, err = script.Echo(string(jsonData)).JQ("{public_key: .pk, secret_key: .sk, dmsg: {discovery: .dmsg.discovery, sessions_count: .dmsg.sessions_count, servers: .dmsg.servers}, transport_discovery: .transport.discovery, log_level: .log_level}").Bytes() + if err != nil { + log.Fatalf("Failed to convert config to setup-node config format: %v", err) + } } // Write the JSON data back to the file err = os.WriteFile(confPath, jsonData, 0644) //nolint @@ -1151,17 +1040,35 @@ var genConfigCmd = &cobra.Command{ // Print results. j, err := json.MarshalIndent(conf, "", "\t") if err != nil { - log.WithError(err).Fatal("Could not unmarshal json.") + log.WithError(err).Fatal("Failed to marshal config to indented JSON") + } + if snConfig { + j, err = script.Echo(string(j)).JQ("{public_key: .pk, secret_key: .sk, dmsg: {discovery: .dmsg.discovery, sessions_count: .dmsg.sessions_count, servers: .dmsg.servers}, transport_discovery: .transport.discovery, log_level: .log_level}").Bytes() + if err != nil { + log.Fatalf("Failed to convert config to setup-node config format: %v", err) + } + var data any + if err = json.Unmarshal(j, &data); err != nil { + log.Fatalf("Failed to convert config to setup-node config format: %v", err) + } + j, err = json.MarshalIndent(data, "", " ") + if err != nil { + log.WithError(err).Fatal("Failed to marshal config to indented JSON") + } } //print config to stdout, omit logging messages, exit if isStdout { - fmt.Printf("%s", j) - os.Exit(0) + if isSquash { + script.Echo(strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(string(j), " ", ""), "\n", ""), "\t", "")).Stdout() //nolint + return + } + script.Echo(string(j)).Stdout() //nolint + return } //hide the printing of the config to the terminal if isHide { log.Infof("Updated file '%s'\n", output) - os.Exit(0) + return } //default behavior log.Infof("Updated file '%s' to:\n%s\n", output, j) @@ -1226,10 +1133,16 @@ const envfileLinux = `# #-- Set custom service conf URLs #SVCCONFADDR=('') +#-- fallback service conf path +#SVCCONF="services-config.json" + #-- Set visor runtime log level. # Default is info ; uncomment for debug logging #LOGLVL=debug +#-- dmsghttp config path +#DMSGCONF="dmsghttp-config.json" + #-- Use dmsghttp to connect to the production deployment #DMSGHTTP=true @@ -1328,10 +1241,16 @@ const envfileWindows = `# #-- Set custom service conf URLs #$SVCCONFADDR= @('') +#-- fallback service conf path +#$SVCCONF='services-config.json' + #-- Set visor runtime log level. # Default is info ; uncomment for debug logging #$LOGLVL=debug +#-- dmsghttp config path +#$DMSGCONF='dmsghttp-config.json' + #-- Use dmsghttp to connect to the production deployment #$DMSGHTTP=true diff --git a/cmd/skywire-cli/commands/config/root.go b/cmd/skywire-cli/commands/config/root.go index 33c8d2ac8..d08fc195e 100644 --- a/cmd/skywire-cli/commands/config/root.go +++ b/cmd/skywire-cli/commands/config/root.go @@ -2,8 +2,11 @@ package cliconfig import ( + "fmt" + "strconv" "strings" + "github.com/bitfield/script" "github.com/skycoin/dmsg/pkg/disc" "github.com/spf13/cobra" @@ -21,6 +24,7 @@ var ( Test: visorconfig.DmsgHTTPServersData{DMSGServers: []*disc.Entry{}}, Prod: visorconfig.DmsgHTTPServersData{DMSGServers: []*disc.Entry{}}, } + path string noFetch bool noDefaults bool stcprPort int @@ -30,6 +34,7 @@ var ( confPath string configName string //nolint Note: configName used, but golangci-lint marked it unused in wrong isStdout bool + isSquash bool isRegen bool isRetainHypervisors bool isTestEnv bool @@ -52,6 +57,7 @@ var ( isBestProtocol bool serviceConfURL string services visorconfig.Services + servicesConfig servicesConf isForce bool isHide bool isAll bool @@ -94,6 +100,8 @@ var ( proxyServerPass string proxyClientPass string configServicePath string + dmsgHTTPPath string + snConfig bool ) // RootCmd contains commands that interact with the config of local skywire-visor @@ -102,3 +110,124 @@ var RootCmd = &cobra.Command{ Short: "Generate or update a skywire config", Long: "Generate or update the config file used by skywire-visor.", } + +type servicesConf struct { //nolint + Test visorconfig.Services `json:"test"` + Prod visorconfig.Services `json:"prod"` +} + +func scriptExecString(s string) string { + if visorconfig.OS == "windows" { + var variable, defaultvalue string + if strings.Contains(s, ":-") { + parts := strings.SplitN(s, ":-", 2) + variable = parts[0] + "}" + defaultvalue = strings.TrimRight(parts[1], "}") + } else { + variable = s + defaultvalue = "" + } + out, err := script.Exec(fmt.Sprintf(`powershell -c '$SKYENV = "%s"; if ($SKYENV -ne "" -and (Test-Path $SKYENV)) { . $SKYENV }; echo %s"`, skyenvfile, variable)).String() + if err == nil { + if (out == "") || (out == variable) { + return defaultvalue + } + return strings.TrimRight(out, "\n") + } + return defaultvalue + } + z, err := script.Exec(fmt.Sprintf(`bash -c 'SKYENV=%s ; if [[ $SKYENV != "" ]] && [[ -f $SKYENV ]] ; then source $SKYENV ; fi ; printf "%s"'`, skyenvfile, s)).String() + if err == nil { + return strings.TrimSpace(z) + } + return "" +} + +func scriptExecBool(s string) bool { + if visorconfig.OS == "windows" { + var variable string + if strings.Contains(s, ":-") { + parts := strings.SplitN(s, ":-", 2) + variable = parts[0] + "}" + } else { + variable = s + } + out, err := script.Exec(fmt.Sprintf(`powershell -c '$SKYENV = "%s"; if ($SKYENV -ne "" -and (Test-Path $SKYENV)) { . $SKYENV }; echo %s"`, skyenvfile, variable)).String() + if err == nil { + if (out == "") || (out == variable) { + return false + } + b, err := strconv.ParseBool(strings.TrimSpace(strings.TrimRight(out, "\n"))) + if err == nil { + return b + } + } + return false + } + z, err := script.Exec(fmt.Sprintf(`bash -c 'SKYENV=%s ; if [[ $SKYENV != "" ]] && [[ -f $SKYENV ]] ; then source $SKYENV ; fi ; printf "%s"'`, skyenvfile, s)).String() + if err == nil { + b, err := strconv.ParseBool(z) + if err == nil { + return b + } + } + + return false +} + +func scriptExecArray(s string) string { + if visorconfig.OS == "windows" { + variable := s + if strings.Contains(variable, "[@]}") { + variable = strings.TrimRight(variable, "[@]}") + variable = strings.TrimRight(variable, "{") + } + out, err := script.Exec(fmt.Sprintf(`powershell -c '$SKYENV = "%s"; if ($SKYENV -ne "" -and (Test-Path $SKYENV)) { . $SKYENV }; foreach ($item in %s) { Write-Host $item }'`, skyenvfile, variable)).Slice() + if err == nil { + if len(out) != 0 { + return "" + } + return strings.Join(out, ",") + } + } + y, err := script.Exec(fmt.Sprintf(`bash -c 'SKYENV=%s ; if [[ $SKYENV != "" ]] && [[ -f $SKYENV ]] ; then source $SKYENV ; fi ; for _i in %s ; do echo "$_i" ; done'`, skyenvfile, s)).Slice() + if err == nil { + return strings.Join(y, ",") + } + return "" +} + +func scriptExecInt(s string) int { + if visorconfig.OS == "windows" { + var variable string + if strings.Contains(s, ":-") { + parts := strings.SplitN(s, ":-", 2) + variable = parts[0] + "}" + } else { + variable = s + } + out, err := script.Exec(fmt.Sprintf(`powershell -c '$SKYENV = "%s"; if ($SKYENV -ne "" -and (Test-Path $SKYENV)) { . $SKYENV }; echo %s"`, skyenvfile, variable)).String() + if err == nil { + if (out == "") || (out == variable) { + return 0 + } + i, err := strconv.Atoi(strings.TrimSpace(strings.TrimRight(out, "\n"))) + if err == nil { + return i + } + return 0 + } + return 0 + } + z, err := script.Exec(fmt.Sprintf(`bash -c 'SKYENV=%s ; if [[ $SKYENV != "" ]] && [[ -f $SKYENV ]] ; then source $SKYENV ; fi ; printf "%s"'`, skyenvfile, s)).String() + if err == nil { + if z == "" { + return 0 + } + i, err := strconv.Atoi(z) + if err == nil { + return i + } + } + return 0 +} diff --git a/cmd/skywire-cli/commands/config/services.go b/cmd/skywire-cli/commands/config/services.go new file mode 100644 index 000000000..96c96dafc --- /dev/null +++ b/cmd/skywire-cli/commands/config/services.go @@ -0,0 +1,91 @@ +// Package cliconfig cmd/skywire-cli/commands/config/services.go +package cliconfig + +import ( + "context" + "encoding/json" + "io" + "net/http" + "os" + + "github.com/spf13/cobra" + + "github.com/skycoin/skywire-utilities/pkg/cmdutil" + "github.com/skycoin/skywire-utilities/pkg/logging" + "github.com/skycoin/skywire-utilities/pkg/skyenv" + "github.com/skycoin/skywire/pkg/visor/visorconfig" +) + +func init() { + updateCmd.AddCommand(servicesCmd) + servicesCmd.Flags().SortFlags = false + //TODO: fix path for non linux package defaults + servicesCmd.Flags().StringVarP(&path, "path", "p", "/opt/skywire/services-config.json", "path of services-config file, default is for pkg installation") +} + +var servicesCmd = &cobra.Command{ + Use: "svc", + Short: "update services-config.json file from config bootstrap service", + Run: func(cmd *cobra.Command, args []string) { + log := logging.MustGetLogger("services_updater") + + ctx, cancel := cmdutil.SignalContext(context.Background(), log) + defer cancel() + go func() { + <-ctx.Done() + cancel() + os.Exit(1) + }() + + servicesConf, err := fetchServicesConf() + if err != nil { + log.WithError(err).Error("Cannot fetching updated services-config data") + } + + file, err := json.MarshalIndent(servicesConf, "", " ") + if err != nil { + log.WithError(err).Error("Error accurs during marshal content to json file") + } + + err = os.WriteFile(path, file, 0600) + if err != nil { + log.WithError(err).Errorf("Cannot save new services-config.json file at %s", path) + } + }, +} + +func fetchServicesConf() (servicesConf, error) { + var newConf servicesConf + var prodConf visorconfig.Services + prodResp, err := http.Get(skyenv.ServiceConfAddr) + if err != nil { + return newConf, err + } + defer prodResp.Body.Close() //nolint + body, err := io.ReadAll(prodResp.Body) + if err != nil { + return newConf, err + } + err = json.Unmarshal(body, &prodConf) + if err != nil { + return newConf, err + } + newConf.Prod = prodConf + + var testConf visorconfig.Services + testResp, err := http.Get(skyenv.TestServiceConfAddr) + if err != nil { + return newConf, err + } + defer testResp.Body.Close() //nolint + body, err = io.ReadAll(testResp.Body) + if err != nil { + return newConf, err + } + err = json.Unmarshal(body, &testConf) + if err != nil { + return newConf, err + } + newConf.Test = testConf + return newConf, nil +} diff --git a/cmd/skywire-cli/commands/log/root.go b/cmd/skywire-cli/commands/log/root.go index 4345095c9..b7dbc7857 100644 --- a/cmd/skywire-cli/commands/log/root.go +++ b/cmd/skywire-cli/commands/log/root.go @@ -22,7 +22,6 @@ import ( "github.com/skycoin/dmsg/pkg/dmsghttp" "github.com/spf13/cobra" - "github.com/skycoin/skywire-utilities/pkg/buildinfo" "github.com/skycoin/skywire-utilities/pkg/cipher" "github.com/skycoin/skywire-utilities/pkg/cmdutil" "github.com/skycoin/skywire-utilities/pkg/logging" @@ -57,12 +56,12 @@ func init() { logCmd.Flags().StringVarP(&fetchFrom, "pks", "k", "", "fetch only from specific public keys ; semicolon separated") logCmd.Flags().StringVarP(&writeDir, "dir", "d", "log_collecting", "save files to specified dir") logCmd.Flags().BoolVarP(&deleteOnErrors, "clean", "c", false, "delete files and folders on errors") - logCmd.Flags().StringVar(&minv, "minv", buildinfo.Version(), "minimum visor version to fetch from") + logCmd.Flags().StringVar(&minv, "minv", "v1.3.15", "minimum visor version to fetch from") logCmd.Flags().StringVar(&incVer, "include-versions", "", "list of version that not satisfy our minimum version condition, but we want include them") logCmd.Flags().IntVarP(&duration, "duration", "n", 0, "number of days before today to fetch transport logs for") logCmd.Flags().BoolVar(&allVisors, "all", false, "consider all visors ; no version filtering") logCmd.Flags().IntVar(&batchSize, "batchSize", 50, "number of visor in each batch") - logCmd.Flags().Int64Var(&maxFileSize, "maxfilesize", 30, "maximum file size allowed to download during collecting logs, in KB") + logCmd.Flags().Int64Var(&maxFileSize, "maxfilesize", 1024, "maximum file size allowed to download during collecting logs, in KB") logCmd.Flags().StringVarP(&dmsgDisc, "dmsg-disc", "D", skyenv.DmsgDiscAddr, "dmsg discovery url\n") logCmd.Flags().StringVarP(&utAddr, "ut", "u", "", "custom uptime tracker url") if os.Getenv("DMSGCURL_SK") != "" { @@ -80,6 +79,10 @@ var logCmd = &cobra.Command{ Long: "Fetch health, survey, and transport logging from visors which are online in the uptime tracker\nhttp://ut.skywire.skycoin.com/uptimes?v=v2\nhttp://ut.skywire.skycoin.com/uptimes?v=v2&visors=;;", Run: func(cmd *cobra.Command, args []string) { log := logging.MustGetLogger("log-collecting") + fver, err := version.NewVersion("v1.3.17") + if err != nil { + log.Fatal("can't parse version for filtering fetches") + } if logOnly && surveyOnly { log.Fatal("use of mutually exclusive flags --log and --survey") } @@ -164,6 +167,10 @@ var logCmd = &cobra.Command{ if v.Online { if fetchFile == "" { visorVersion, err := version.NewVersion(v.Version) //nolint + if v.Version == "" { + log.Warnf("The version for visor %s is blank", v.PubKey) + continue + } includeV := contains(incVerList, v.Version) if err != nil && !includeV { log.Warnf("The version %s for visor %s is not valid", v.Version, v.PubKey) @@ -199,7 +206,11 @@ var logCmd = &cobra.Command{ } } if !logOnly { - download(ctx, log, httpC, "node-info.json", "node-info.json", key, maxFileSize) //nolint + if visorVersion.LessThan(fver) { + download(ctx, log, httpC, "node-info.json", "node-info.json", key, maxFileSize) //nolint + } else { + download(ctx, log, httpC, "node-info", "node-info.json", key, maxFileSize) //nolint + } } if !surveyOnly { for i := 0; i <= duration; i++ { @@ -337,8 +348,10 @@ func (pw *ProgressWriter) Write(p []byte) (int, error) { func getUptimes(endpoint string, log *logging.Logger) ([]VisorUptimeResponse, error) { var results []VisorUptimeResponse - - response, err := http.Get(endpoint) //nolint + client := http.Client{ + Timeout: 60 * time.Second, + } + response, err := client.Get(endpoint) //nolint if err != nil { log.Error("Error while fetching data from uptime service. Error: ", err) return results, errors.New("Cannot get Uptime data") diff --git a/cmd/skywire-cli/commands/proxy/proxy.go b/cmd/skywire-cli/commands/proxy/proxy.go index 7444d507b..69feb2938 100644 --- a/cmd/skywire-cli/commands/proxy/proxy.go +++ b/cmd/skywire-cli/commands/proxy/proxy.go @@ -4,18 +4,16 @@ package skysocksc import ( "bytes" "context" - "encoding/json" "fmt" - "math/rand" - "net/http" "os" "strings" "text/tabwriter" "time" + "github.com/bitfield/script" "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/spf13/pflag" + "github.com/tidwall/pretty" "github.com/skycoin/skywire-utilities/pkg/buildinfo" "github.com/skycoin/skywire-utilities/pkg/cmdutil" @@ -24,7 +22,7 @@ import ( "github.com/skycoin/skywire/cmd/skywire-cli/internal" "github.com/skycoin/skywire/pkg/app/appserver" "github.com/skycoin/skywire/pkg/routing" - "github.com/skycoin/skywire/pkg/servicedisc" + "github.com/skycoin/skywire/pkg/visor/visorconfig" ) func init() { @@ -37,21 +35,15 @@ func init() { ) version := buildinfo.Version() if version == "unknown" { - version = "" + version = "" //nolint } startCmd.Flags().StringVarP(&pk, "pk", "k", "", "server public key") startCmd.Flags().StringVarP(&addr, "addr", "a", "", "address of proxy for use") startCmd.Flags().StringVarP(&clientName, "name", "n", "", "name of skysocks client") + startCmd.Flags().IntVarP(&startingTimeout, "timeout", "t", 0, "timeout for starting proxy") + startCmd.Flags().StringVar(&httpAddr, "http", "", "address for http proxy") stopCmd.Flags().BoolVar(&allClients, "all", false, "stop all skysocks client") stopCmd.Flags().StringVar(&clientName, "name", "", "specific skysocks client that want stop") - listCmd.Flags().StringVarP(&sdURL, "url", "a", "", "service discovery url default:\n"+skyenv.ServiceDiscAddr) - listCmd.Flags().BoolVarP(&directQuery, "direct", "b", false, "query service discovery directly") - listCmd.Flags().StringVarP(&pk, "pk", "k", "", "check "+serviceType+" service discovery for public key") - listCmd.Flags().IntVarP(&count, "num", "n", 0, "number of results to return (0 = all)") - listCmd.Flags().BoolVarP(&isUnFiltered, "unfilter", "u", false, "provide unfiltered results") - listCmd.Flags().StringVarP(&ver, "ver", "v", version, "filter results by version") - listCmd.Flags().StringVarP(&country, "country", "c", "", "filter results by country") - listCmd.Flags().BoolVarP(&isStats, "stats", "s", false, "return only a count of the results") } var startCmd = &cobra.Command{ @@ -64,8 +56,27 @@ var startCmd = &cobra.Command{ internal.PrintFatalError(cmd.Flags(), fmt.Errorf("unable to create RPC client: %w", err)) } - if clientName != "" && pk != "" && addr != "" { - // add new app with -srv and -addr args, and if app was there just change -srv and -addr args and run it + // stop possible running proxy before start it again + if clientName != "" { + rpcClient.StopApp(clientName) //nolint + } else { + rpcClient.StopApp("skysocks-client") //nolint + } + + tCtx := context.Background() //nolint + if startingTimeout != 0 { + tCtx, _ = context.WithTimeout(context.Background(), time.Duration(startingTimeout)*time.Second) //nolint + } + ctx, cancel := cmdutil.SignalContext(tCtx, &logrus.Logger{}) + go func() { + <-ctx.Done() + cancel() + rpcClient.KillApp(clientName) //nolint + fmt.Print("\nStopped!") + os.Exit(1) + }() + + if pk != "" { err := pubkey.Set(pk) if err != nil { if len(args) > 0 { @@ -77,10 +88,23 @@ var startCmd = &cobra.Command{ internal.PrintFatalError(cmd.Flags(), fmt.Errorf("Invalid or missing public key")) } } + arguments := map[string]any{} + arguments["app"] = "skysocks-client" + + arguments["--srv"] = pubkey.String() - arguments := map[string]string{} - arguments["srv"] = pubkey.String() - arguments["addr"] = addr + if addr == "" { + addr = visorconfig.SkysocksClientAddr + } + arguments["--addr"] = addr + + if httpAddr != "" { + arguments["--http"] = httpAddr + } + + if clientName == "" { + clientName = "skysocks-client" + } _, err = rpcClient.App(clientName) if err == nil { @@ -89,7 +113,7 @@ var startCmd = &cobra.Command{ internal.PrintFatalError(cmd.Flags(), fmt.Errorf("Error occurs during set args to custom skysocks client")) } } else { - err = rpcClient.AddApp(clientName, "skysocks-client") + err = rpcClient.AddApp(clientName, "skywire") if err != nil { internal.PrintFatalError(cmd.Flags(), fmt.Errorf("Error during add new app")) } @@ -100,38 +124,14 @@ var startCmd = &cobra.Command{ } internal.Catch(cmd.Flags(), rpcClient.StartApp(clientName)) internal.PrintOutput(cmd.Flags(), nil, "Starting.") - } else if clientName != "" && pk == "" && addr == "" { - internal.Catch(cmd.Flags(), rpcClient.StartApp(clientName)) - internal.PrintOutput(cmd.Flags(), nil, "Starting.") - } else if pk != "" && clientName == "" && addr == "" { - err := pubkey.Set(pk) - if err != nil { - if len(args) > 0 { - err := pubkey.Set(args[0]) - if err != nil { - internal.PrintFatalError(cmd.Flags(), err) - } - } else { - internal.PrintFatalError(cmd.Flags(), fmt.Errorf("Invalid or missing public key")) - } + } else { + if clientName == "" { + clientName = "skysocks-client" } - internal.Catch(cmd.Flags(), rpcClient.StartSkysocksClient(pubkey.String())) + internal.Catch(cmd.Flags(), rpcClient.StartApp(clientName)) internal.PrintOutput(cmd.Flags(), nil, "Starting.") - clientName = "skysocks-client" - // change defaul skysocks-proxy app -srv arg and run it - } else { - // error - return } - ctx, cancel := cmdutil.SignalContext(context.Background(), &logrus.Logger{}) - go func() { - <-ctx.Done() - cancel() - rpcClient.StopApp(clientName) //nolint - os.Exit(1) - }() - startProcess := true for startProcess { time.Sleep(time.Second * 1) @@ -156,6 +156,13 @@ var startCmd = &cobra.Command{ } internal.PrintOutput(cmd.Flags(), out, fmt.Sprintln("\nError! > "+state.DetailedStatus)) } + if state.Status == appserver.AppStatusStopped { + startProcess = false + out := output{ + AppError: state.DetailedStatus, + } + internal.PrintOutput(cmd.Flags(), out, fmt.Sprintln("\nStopped!"+state.DetailedStatus)) + } } } } @@ -211,33 +218,35 @@ var statusCmd = &cobra.Command{ var jsonAppStatus []appState fmt.Fprintf(w, "---- All Proxy List -----------------------------------------------------\n\n") for _, state := range states { - if state.AppConfig.Binary == binaryName { - status := "stopped" - if state.Status == appserver.AppStatusRunning { - status = "running" - } - if state.Status == appserver.AppStatusErrored { - status = "errored" - } - jsonAppStatus = append(jsonAppStatus, appState{ - Name: state.Name, - Status: status, - AutoStart: state.AutoStart, - Args: state.Args, - AppPort: state.Port, - }) - var tmpAddr string - var tmpSrv string - for idx, arg := range state.Args { - if arg == "-srv" { - tmpSrv = state.Args[idx+1] + for _, v := range state.AppConfig.Args { + if v == binaryName { + status := "stopped" + if state.Status == appserver.AppStatusRunning { + status = "running" } - if arg == "-addr" { - tmpAddr = "127.0.0.1" + state.Args[idx+1] + if state.Status == appserver.AppStatusErrored { + status = "errored" + } + jsonAppStatus = append(jsonAppStatus, appState{ + Name: state.Name, + Status: status, + AutoStart: state.AutoStart, + Args: state.Args, + AppPort: state.Port, + }) + var tmpAddr string + var tmpSrv string + for idx, arg := range state.Args { + if arg == "--srv" { + tmpSrv = state.Args[idx+1] + } + if arg == "--addr" { + tmpAddr = "127.0.0.1" + state.Args[idx+1] + } } + _, err = fmt.Fprintf(w, "Name: %s\nStatus: %s\nServer: %s\nAddress: %s\nAppPort: %d\nAutoStart: %t\n\n", state.Name, status, tmpSrv, tmpAddr, state.Port, state.AutoStart) + internal.Catch(cmd.Flags(), err) } - _, err = fmt.Fprintf(w, "Name: %s\nStatus: %s\nServer: %s\nAddress: %s\nAppPort: %d\nAutoStart: %t\n\n", state.Name, status, tmpSrv, tmpAddr, state.Port, state.AutoStart) - internal.Catch(cmd.Flags(), err) } } fmt.Fprintf(w, "-------------------------------------------------------------------------\n") @@ -246,121 +255,95 @@ var statusCmd = &cobra.Command{ }, } +var isLabel bool + +func init() { + if version == "unknown" { + version = "" //nolint + } + version = strings.Split(version, "-")[0] + listCmd.Flags().StringVarP(&utURL, "uturl", "w", skyenv.UptimeTrackerAddr, "uptime tracker url") + listCmd.Flags().StringVarP(&sdURL, "sdurl", "a", skyenv.ServiceDiscAddr, "service discovery url") + listCmd.Flags().BoolVarP(&rawData, "raw", "r", false, "print raw data") + listCmd.Flags().BoolVarP(&noFilterOnline, "noton", "o", false, "do not filter by online status in UT") + listCmd.Flags().StringVar(&cacheFileSD, "cfs", os.TempDir()+"/proxysd.json", "SD cache file location") + listCmd.Flags().StringVar(&cacheFileUT, "cfu", os.TempDir()+"/ut.json", "UT cache file location.") + listCmd.Flags().IntVarP(&cacheFilesAge, "cfa", "m", 5, "update cache files if older than n minutes") + listCmd.Flags().StringVarP(&pk, "pk", "k", "", "check "+serviceType+" service discovery for public key") + listCmd.Flags().BoolVarP(&isUnFiltered, "unfilter", "u", false, "provide unfiltered results") + listCmd.Flags().StringVarP(&ver, "ver", "v", version, "filter results by version") + listCmd.Flags().StringVarP(&country, "country", "c", "", "filter results by country") + listCmd.Flags().BoolVarP(&isStats, "stats", "s", false, "return only a count of the results") + listCmd.Flags().BoolVarP(&isLabel, "label", "l", false, "label keys by country \033[91m(SLOW)\033[0m") +} + var listCmd = &cobra.Command{ Use: "list", Short: "List servers", - Long: "List " + serviceType + " servers from service discovery\n " + skyenv.ServiceDiscAddr + "/api/services?type=" + serviceType + "\n " + skyenv.ServiceDiscAddr + "/api/services?type=" + serviceType + "&country=US", + Long: fmt.Sprintf("List %v servers from service discovery\n%v/api/services?type=%v\n%v/api/services?type=%v&country=US\n\nSet cache file location to \"\" to avoid using cache files", serviceType, skyenv.ServiceDiscAddr, serviceType, skyenv.ServiceDiscAddr, serviceType), Run: func(cmd *cobra.Command, args []string) { - //validate any specified public key + sds := internal.GetData(cacheFileSD, sdURL+"/api/services?type="+serviceType, cacheFilesAge) + if rawData { + script.Echo(string(pretty.Color(pretty.Pretty([]byte(sds)), nil))).Stdout() //nolint + return + } if pk != "" { - err := pubkey.Set(pk) - if err != nil { - internal.PrintFatalError(cmd.Flags(), fmt.Errorf("Invalid or missing public key")) + if isStats { + count, _ := script.Echo(sds).JQ(`map(select(.address == "`+pk+`:3"))`).Replace("\"", "").Replace(":", " ").Column(1).CountLines() //nolint + script.Echo(fmt.Sprintf("%v\n", count)).Stdout() //nolint + return } + jsonOut, _ := script.Echo(sds).JQ(`map(select(.address == "` + pk + `:3"))`).Bytes() //nolint + script.Echo(string(pretty.Color(pretty.Pretty(jsonOut), nil))).Stdout() //nolint + return } - if sdURL == "" { - sdURL = skyenv.ServiceDiscAddr - } - if isUnFiltered { - ver = "" - country = "" + var sdJQ string + if !isUnFiltered { + if ver != "" && country == "" { + sdJQ = `select(.version == "` + ver + `")` + } + if country != "" && ver == "" { + sdJQ = `select(.geo.country == "` + country + `")` + } + if country != "" && ver != "" { + sdJQ = `select(.geo.country == "` + country + `" and .version == "` + ver + `")` + } } - if directQuery { - servers = directQuerySD(cmd.Flags()) + if sdJQ != "" { + sdJQ = `.[] | ` + sdJQ + ` | .address` } else { - rpcClient, err := clirpc.Client(cmd.Flags()) - if err != nil { - internal.PrintError(cmd.Flags(), fmt.Errorf("unable to create RPC client: %w", err)) - internal.PrintOutput(cmd.Flags(), fmt.Sprintf("directly querying service discovery\n%s/api/services?type=%s\n", sdURL, serviceType), fmt.Sprintf("directly querying service discovery\n%s/api/services?type=%s\n", sdURL, serviceType)) - servers = directQuerySD(cmd.Flags()) - } else { - servers, err = rpcClient.ProxyServers(ver, country) - if err != nil { - internal.PrintError(cmd.Flags(), err) - internal.PrintOutput(cmd.Flags(), fmt.Sprintf("directly querying service discovery\n%s/api/services?type=%s\n", sdURL, serviceType), fmt.Sprintf("directly querying service discovery\n%s/api/services?type=%s\n", sdURL, serviceType)) - servers = directQuerySD(cmd.Flags()) - } - } + sdJQ = `.[] .address` } - if len(servers) == 0 { - internal.PrintOutput(cmd.Flags(), "No Servers found", "No Servers found") - os.Exit(0) + var sdkeys string + sdkeys, _ = script.Echo(sds).JQ(sdJQ).Replace("\"", "").Replace(":", " ").Column(1).String() //nolint + if noFilterOnline { + if isStats { + count, _ := script.Echo(sdkeys).CountLines() //nolint + script.Echo(fmt.Sprintf("%v\n", count)).Stdout() //nolint + return + } + script.Echo(sdkeys).Stdout() //nolint + return } + uts := internal.GetData(cacheFileUT, utURL+"/uptimes?v=v2", cacheFilesAge) + utkeys, _ := script.Echo(uts).JQ(".[] | select(.on) | .pk").Replace("\"", "").String() //nolint if isStats { - internal.PrintOutput(cmd.Flags(), fmt.Sprintf("%d Servers\n", len(servers)), fmt.Sprintf("%d Servers\n", len(servers))) + count, _ := script.Echo(sdkeys + utkeys).Freq().Match("2 ").Column(2).CountLines() //nolint + script.Echo(fmt.Sprintf("%v\n", count)).Stdout() //nolint + return + } + if !isLabel { + script.Echo(sdkeys + utkeys).Freq().Match("2 ").Column(2).Stdout() //nolint } else { - var msg string - var results []string - limit := len(servers) - if count > 0 && count < limit { - limit = count - } - if pk != "" { - for _, server := range servers { - if strings.Replace(server.Addr.String(), servicePort, "", 1) == pk { - results = append(results, server.Addr.String()) - } - } - } else { - for _, server := range servers { - results = append(results, server.Addr.String()) + filteredKeys, _ := script.Echo(sdkeys + utkeys).Freq().Match("2 ").Column(2).Slice() //nolint + formattedoutput, _ := script.Echo(sds).JQ(".[] | \"\\(.address) \\(.geo.country)\"").Replace("\"", "").Slice() //nolint + // Very slow! + for _, fo := range formattedoutput { + for _, fk := range filteredKeys { + script.Echo(fo).Match(fk).Stdout() //nolint } } - - //randomize the order of the displayed results - rand.Shuffle(len(results), func(i, j int) { - results[i], results[j] = results[j], results[i] - }) - for i := 0; i < limit && i < len(results); i++ { - msg += strings.Replace(results[i], servicePort, "", 1) - if server := findServerByPK(servers, results[i]); server != nil && server.Geo != nil { - if server.Geo.Country != "" { - msg += fmt.Sprintf(" | %s\n", server.Geo.Country) - } else { - msg += "\n" - } - } else { - msg += "\n" - } - } - internal.PrintOutput(cmd.Flags(), servers, msg) } - }, -} -func directQuerySD(cmdFlags *pflag.FlagSet) (s []servicedisc.Service) { - //url/uri format - //https://sd.skycoin.com/api/services?type=proxy&country=US&version=v1.3.7 - sdURL += "/api/services?type=" + serviceType - if country != "" { - sdURL += "&country=" + country - } - if ver != "" { - sdURL += "&version=" + ver - } - //preform http get request for the service discovery URL - resp, err := (&http.Client{Timeout: time.Duration(30 * time.Second)}).Get(sdURL) - if err != nil { - internal.PrintFatalError(cmdFlags, fmt.Errorf("error fetching servers from service discovery: %w", err)) - } - defer func() { - if err := resp.Body.Close(); err != nil { - internal.PrintError(cmdFlags, fmt.Errorf("error closing http response body: %w", err)) - } - }() - // Decode JSON response into struct - err = json.NewDecoder(resp.Body).Decode(&s) - if err != nil { - internal.PrintFatalError(cmdFlags, fmt.Errorf("error decoding json to struct: %w", err)) - } - return s -} - -func findServerByPK(servers []servicedisc.Service, addr string) *servicedisc.Service { - for _, server := range servers { - if server.Addr.String() == addr { - return &server - } - } - return nil + }, } diff --git a/cmd/skywire-cli/commands/proxy/root.go b/cmd/skywire-cli/commands/proxy/root.go index d6d5a95a2..0caa1ee24 100644 --- a/cmd/skywire-cli/commands/proxy/root.go +++ b/cmd/skywire-cli/commands/proxy/root.go @@ -4,28 +4,34 @@ package skysocksc import ( "github.com/spf13/cobra" + "github.com/skycoin/skywire-utilities/pkg/buildinfo" "github.com/skycoin/skywire-utilities/pkg/cipher" "github.com/skycoin/skywire/pkg/servicedisc" ) var ( - binaryName = "skysocks-client" - stateName = "skysocks-client" - serviceType = servicedisc.ServiceTypeProxy - servicePort = ":44" - isUnFiltered bool - ver string - country string - isStats bool - pubkey cipher.PubKey - pk string - count int - sdURL string - directQuery bool - servers []servicedisc.Service - allClients bool - clientName string - addr string + version = buildinfo.Version() + binaryName = "skysocks-client" + stateName = "skysocks-client" + serviceType = servicedisc.ServiceTypeProxy + isUnFiltered bool + rawData bool + utURL string + sdURL string + cacheFileSD string + cacheFileUT string + cacheFilesAge int + ver string + country string + isStats bool + pubkey cipher.PubKey + pk string + allClients bool + noFilterOnline bool + clientName string + addr string + startingTimeout int + httpAddr string ) // RootCmd contains commands that interact with the skywire-visor diff --git a/cmd/skywire-cli/commands/reward/root.go b/cmd/skywire-cli/commands/reward/root.go index 374cc405c..388fb167b 100644 --- a/cmd/skywire-cli/commands/reward/root.go +++ b/cmd/skywire-cli/commands/reward/root.go @@ -3,14 +3,9 @@ package clireward import ( "fmt" - "log" "os" - "sort" - "strconv" "strings" - "time" - "github.com/bitfield/script" coincipher "github.com/skycoin/skycoin/src/cipher" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -200,162 +195,3 @@ func readRewardFile(cmdFlags *pflag.FlagSet) { output := fmt.Sprintf("Reward address file:\n %s\nreward address:\n %s\n", output, dat) internal.PrintOutput(cmdFlags, output, output) } - -const yearlyTotalRewards int = 408000 - -var ( - yearlyTotal int - surveyPath string - wdate = time.Now().AddDate(0, 0, -1).Format("2006-01-02") - utfile string - disallowArchitectures string -) - -type nodeinfo struct { - SkyAddr string `json:"skycoin_address"` - PK string `json:"public_key"` - Arch string `json:"go_arch"` - IPAddr string `json:"ip_address"` - Share float64 `json:"reward_share"` - Reward float64 `json:"reward_amount"` -} - -type ipCount struct { - IP string - Count int -} - -type rewardData struct { - SkyAddr string - Reward float64 -} - -func init() { - RootCmd.AddCommand(rewardCalcCmd) - rewardCalcCmd.Flags().SortFlags = false - rewardCalcCmd.Flags().StringVarP(&wdate, "date", "d", wdate, "date for which to calculate reward") - rewardCalcCmd.Flags().StringVarP(&disallowArchitectures, "noarch", "n", "amd64", "disallowed architectures, comma separated") - rewardCalcCmd.Flags().IntVarP(&yearlyTotal, "year", "y", yearlyTotalRewards, "yearly total rewards") - rewardCalcCmd.Flags().StringVarP(&utfile, "utfile", "u", "ut.txt", "uptime tracker data file") - rewardCalcCmd.Flags().StringVarP(&surveyPath, "path", "p", "./log_collecting", "path to the surveys ") -} - -var rewardCalcCmd = &cobra.Command{ - Use: "calc", - Short: "calculate rewards from uptime data & collected surveys", - Long: ` -Collect surveys: skywire-cli log -Fetch uptimes: skywire-cli ut > ut.txt`, - Run: func(cmd *cobra.Command, args []string) { - _, err := os.Stat(surveyPath) - if os.IsNotExist(err) { - log.Fatal("the path to the surveys does not exist\n", err, "\nfetch the surveys with:\n$ skywire-cli log") - } - _, err = os.Stat(utfile) - if os.IsNotExist(err) { - log.Fatal("uptime tracker data file not found\n", err, "\nfetch the uptime tracker data with:\n$ skywire-cli ut > ut.txt") - } - - archMap := make(map[string]struct{}) - for _, disallowedarch := range strings.Split(disallowArchitectures, ",") { - if disallowedarch != "" { - archMap[disallowedarch] = struct{}{} - } - } - res, _ := script.File(utfile).Match(strings.TrimRight(wdate, "\n")).Column(1).Slice() //nolint - var nodesInfos []nodeinfo - for _, pk := range res { - nodeInfo := fmt.Sprintf("%s/%s/node-info.json", surveyPath, pk) - ip, _ := script.File(nodeInfo).JQ(`."ip.skycoin.com".ip_address`).Replace(" ", "").Replace(`"`, "").String() //nolint - ip = strings.TrimRight(ip, "\n") - sky, _ := script.File(nodeInfo).JQ(".skycoin_address").Replace(" ", "").Replace(`"`, "").String() //nolint - sky = strings.TrimRight(sky, "\n") - arch, _ := script.File(nodeInfo).JQ(".go_arch").Replace(" ", "").Replace(`"`, "").String() //nolint - arch = strings.TrimRight(arch, "\n") - if _, disallowed := archMap[arch]; !disallowed && ip != "" && strings.Count(ip, ".") == 3 && sky != "" { - ni := nodeinfo{ - IPAddr: ip, - SkyAddr: sky, - PK: pk, - Arch: arch, - } - nodesInfos = append(nodesInfos, ni) - } - } - daysThisMonth := float64(time.Date(time.Now().Year(), time.Now().Month()+1, 0, 0, 0, 0, 0, time.UTC).Day()) - daysThisYear := float64(int(time.Date(time.Now().Year(), 12, 31, 23, 59, 59, 999999999, time.UTC).Sub(time.Date(time.Now().Year(), 1, 1, 0, 0, 0, 0, time.UTC)).Hours()) / 24) - monthReward := (float64(yearlyTotal) / daysThisYear) * daysThisMonth - dayReward := monthReward / daysThisMonth - wdate = strings.ReplaceAll(wdate, " ", "0") - fmt.Printf("date: %s\n", wdate) - fmt.Printf("days this month: %.4f\n", daysThisMonth) - fmt.Printf("days in the year: %.4f\n", daysThisYear) - fmt.Printf("this month's rewards: %.4f\n", monthReward) - fmt.Printf("reward total: %.4f\n", dayReward) - - uniqueIP, _ := script.Echo(func() string { //nolint - var inputStr strings.Builder - for _, ni := range nodesInfos { - inputStr.WriteString(fmt.Sprintf("%s\n", ni.IPAddr)) - } - return inputStr.String() - }()).Freq().String() //nolint - var ipCounts []ipCount - lines := strings.Split(uniqueIP, "\n") - for _, line := range lines { - if line != "" { - fields := strings.Fields(line) - if len(fields) == 2 { - count, _ := strconv.Atoi(fields[0]) //nolint - ipCounts = append(ipCounts, ipCount{ - IP: fields[1], - Count: count, - }) - } - } - } - totalValidShares := 0 - for _, ipCount := range ipCounts { - if ipCount.Count <= 8 { - totalValidShares += ipCount.Count - } else { - totalValidShares += 8 - } - } - fmt.Printf("Total valid shares: %d\n", totalValidShares) - - for i, ni := range nodesInfos { - for _, ipCount := range ipCounts { - if ni.IPAddr == ipCount.IP { - if ipCount.Count <= 8 { - nodesInfos[i].Share = 1.0 - } else { - nodesInfos[i].Share = 8.0 / float64(ipCount.Count) - } - break - } - } - nodesInfos[i].Reward = nodesInfos[i].Share / float64(totalValidShares) * dayReward - } - - fmt.Println("IP, Skycoin Address, Skywire Public Key, Architecture, Reward Shares, Reward $SKY amout") - for _, ni := range nodesInfos { - fmt.Printf("%s, %s, %s, %s, %4f, %4f\n", ni.IPAddr, ni.SkyAddr, ni.PK, ni.Arch, ni.Share, ni.Reward) - } - rewardSumBySkyAddr := make(map[string]float64) - for _, ni := range nodesInfos { - rewardSumBySkyAddr[ni.SkyAddr] += ni.Reward - } - var sortedSkyAddrs []rewardData - for skyAddr, rewardSum := range rewardSumBySkyAddr { - sortedSkyAddrs = append(sortedSkyAddrs, rewardData{SkyAddr: skyAddr, Reward: rewardSum}) - } - sort.Slice(sortedSkyAddrs, func(i, j int) bool { - return sortedSkyAddrs[i].Reward > sortedSkyAddrs[j].Reward - }) - fmt.Println("Skycoin Address, Reward Amount") - for _, skyAddrReward := range sortedSkyAddrs { - fmt.Printf("%s, %.4f\n", skyAddrReward.SkyAddr, skyAddrReward.Reward) - } - }, -} diff --git a/cmd/skywire-cli/commands/rewards/calc.go b/cmd/skywire-cli/commands/rewards/calc.go new file mode 100644 index 000000000..d1f5c9382 --- /dev/null +++ b/cmd/skywire-cli/commands/rewards/calc.go @@ -0,0 +1,300 @@ +// Package clirewards cmd/skywire-cli/commands/rewards/calc.go +package clirewards + +import ( + "fmt" + "log" + "os" + "sort" + "strconv" + "strings" + "time" + + "github.com/bitfield/script" + "github.com/spf13/cobra" +) + +const yearlyTotalRewards int = 408000 + +var ( + yearlyTotal int + surveyPath string + wdate = time.Now().AddDate(0, 0, -1).Format("2006-01-02") + wDate time.Time + utfile string + disallowArchitectures string + h0 bool + h1 bool + h2 bool + grr bool + pubkey string +) + +type nodeinfo struct { + SkyAddr string `json:"skycoin_address"` + PK string `json:"public_key"` + Arch string `json:"go_arch"` + Interfaces string `json:"interfaces"` + IPAddr string `json:"ip_address"` + UUID string `json:"uuid"` + Share float64 `json:"reward_share"` + Reward float64 `json:"reward_amount"` + MacAddr string +} + +type counting struct { + Name string + Count int +} + +type rewardData struct { + SkyAddr string + Reward float64 + Shares float64 +} + +func init() { + RootCmd.Flags().SortFlags = false + RootCmd.Flags().StringVarP(&wdate, "date", "d", wdate, "date for which to calculate reward") + RootCmd.Flags().StringVarP(&pubkey, "pk", "k", pubkey, "check reward for pubkey") + RootCmd.Flags().StringVarP(&disallowArchitectures, "noarch", "n", "amd64", "disallowed architectures, comma separated") + RootCmd.Flags().IntVarP(&yearlyTotal, "year", "y", yearlyTotalRewards, "yearly total rewards") + RootCmd.Flags().StringVarP(&utfile, "utfile", "u", "ut.txt", "uptime tracker data file") + RootCmd.Flags().StringVarP(&surveyPath, "path", "p", "log_collecting", "path to the surveys") + RootCmd.Flags().BoolVarP(&h0, "h0", "0", false, "hide statistical data") + RootCmd.Flags().BoolVarP(&h1, "h1", "1", false, "hide survey csv data") + RootCmd.Flags().BoolVarP(&h2, "h2", "2", false, "hide reward csv data") + RootCmd.Flags().BoolVarP(&grr, "err", "e", false, "account for non rewarded keys") +} + +// RootCmd is the root command for skywire-cli rewards +var RootCmd = &cobra.Command{ + Use: "rewards", + Short: "calculate rewards from uptime data & collected surveys", + Long: ` +Collect surveys: skywire-cli log +Fetch uptimes: skywire-cli ut > ut.txt`, + Run: func(cmd *cobra.Command, args []string) { + var err error + wDate, err = time.Parse("2006-01-02", wdate) + if err != nil { + log.Fatal("Error parsing date:", err) + return + } + _, err = os.Stat(surveyPath) + if os.IsNotExist(err) { + log.Fatal("the path to the surveys does not exist\n", err, "\nfetch the surveys with:\n$ skywire-cli log") + } + _, err = os.Stat(utfile) + if os.IsNotExist(err) { + log.Fatal("uptime tracker data file not found\n", err, "\nfetch the uptime tracker data with:\n$ skywire-cli ut > ut.txt") + } + + archMap := make(map[string]struct{}) + for _, disallowedarch := range strings.Split(disallowArchitectures, ",") { + if disallowedarch != "" { + archMap[disallowedarch] = struct{}{} + } + } + var res []string + if pubkey == "" { + res, _ = script.File(utfile).Match(strings.TrimRight(wdate, "\n")).Column(1).Slice() //nolint + if len(res) == 0 { + log.Fatal("No keys achieved minimum uptime on " + wdate + " !") + } + } else { + res, _ = script.File(utfile).Match(strings.TrimRight(wdate, "\n")).Column(1).Match(pubkey).Slice() //nolint + if len(res) == 0 { + log.Fatal("Specified key " + pubkey + "\n did not achieve minimum uptime on " + wdate + " !") + } + } + var nodesInfos []nodeinfo + var grrInfos []nodeinfo + for _, pk := range res { + nodeInfo := fmt.Sprintf("%s/%s/node-info.json", surveyPath, pk) + ip, _ := script.File(nodeInfo).JQ(`."ip.skycoin.com".ip_address`).Replace(" ", "").Replace(`"`, "").String() //nolint + ip = strings.TrimRight(ip, "\n") + sky, _ := script.File(nodeInfo).JQ(".skycoin_address").Replace(" ", "").Replace(`"`, "").String() //nolint + sky = strings.TrimRight(sky, "\n") + arch, _ := script.File(nodeInfo).JQ(".go_arch").Replace(" ", "").Replace(`"`, "").String() //nolint + arch = strings.TrimRight(arch, "\n") + uu, _ := script.File(nodeInfo).JQ(".uuid").Replace(" ", "").Replace(`"`, "").String() //nolint + uu = strings.TrimRight(uu, "\n") + ifc, _ := script.File(nodeInfo).JQ(`[.ip_addr[]? | select(.ifname != "lo") | {address: .address, ifname: .ifname}]`).Replace(" ", "").Replace(`"`, "").String() //nolint + ifc = strings.TrimRight(ifc, "\n") + ifc1, _ := script.File(nodeInfo).JQ(`[.zcalusic_sysinfo.network[] | {address: .macaddress, ifname: .name}]`).Replace(" ", "").Replace(`"`, "").String() //nolint + ifc1 = strings.TrimRight(ifc1, "\n") + macs, _ := script.File(nodeInfo).JQ(`.ip_addr[]? | select(.ifname != "lo") | .address`).Replace(" ", "").Replace(`"`, "").Slice() //nolint + macs1, _ := script.File(nodeInfo).JQ(`.zcalusic_sysinfo.network[] | .macaddress`).Replace(" ", "").Replace(`"`, "").Slice() //nolint + if ifc == "[]" && ifc1 != "[]" { + ifc = ifc1 + } + if len(macs) == 0 && len(macs1) > 0 { + macs = macs1 + } else { + macs = append(macs, "") + } + ni := nodeinfo{ + IPAddr: ip, + SkyAddr: sky, + PK: pk, + Arch: arch, + Interfaces: ifc, + MacAddr: macs[0], + UUID: uu, + } + if _, disallowed := archMap[arch]; !disallowed && ip != "" && strings.Count(ip, ".") == 3 && sky != "" && uu != "" && ifc != "" && len(macs) > 0 && macs[0] != "" { + nodesInfos = append(nodesInfos, ni) + } else { + if grr { + grrInfos = append(grrInfos, ni) + } + } + } + if grr { + for _, ni := range grrInfos { + fmt.Printf("%s, %s, %.6f, %.6f, %s, %s, %s, %s \n", ni.SkyAddr, ni.PK, ni.Share, ni.Reward, ni.IPAddr, ni.Arch, ni.UUID, ni.Interfaces) + } + return + } + daysThisMonth := time.Date(wDate.Year(), wDate.Month()+1, 0, 0, 0, 0, 0, time.UTC).Day() + daysThisYear := int(time.Date(wDate.Year(), 12, 31, 23, 59, 59, 999999999, time.UTC).Sub(time.Date(wDate.Year(), 1, 1, 0, 0, 0, 0, time.UTC)).Hours()) / 24 + monthReward := (float64(yearlyTotal) / float64(daysThisYear)) * float64(daysThisMonth) + dayReward := monthReward / float64(daysThisMonth) + wdate = strings.ReplaceAll(wdate, " ", "0") + if !h0 { + fmt.Printf("date: %s\n", wdate) + fmt.Printf("days this month: %d\n", daysThisMonth) + fmt.Printf("days in the year: %d\n", daysThisYear) + fmt.Printf("this month's rewards: %.6f\n", monthReward) + fmt.Printf("reward total: %.6f\n", dayReward) + } + uniqueIP, _ := script.Echo(func() string { //nolint + var inputStr strings.Builder + for _, ni := range nodesInfos { + inputStr.WriteString(fmt.Sprintf("%s\n", ni.IPAddr)) + } + return inputStr.String() + }()).Freq().Slice() //nolint + var ipCounts []counting + for _, line := range uniqueIP { + if line != "" { + fields := strings.Fields(line) + if len(fields) == 2 { + count, _ := strconv.Atoi(fields[0]) //nolint + ipCounts = append(ipCounts, counting{ + Name: fields[1], + Count: count, + }) + } + } + } + uniqueUUID, _ := script.Echo(func() string { //nolint + var inputStr strings.Builder + for _, ni := range nodesInfos { + inputStr.WriteString(fmt.Sprintf("%s\n", ni.UUID)) + } + return inputStr.String() + }()).Freq().Slice() //nolint + + // look at the first non loopback interface macaddress + uniqueMac, _ := script.Echo(func() string { //nolint + var inputStr strings.Builder + for _, ni := range nodesInfos { + inputStr.WriteString(fmt.Sprintf("%s\n", ni.MacAddr)) + } + return inputStr.String() + }()).Freq().Slice() //nolint + + var macCounts []counting + for _, line := range uniqueMac { + if line != "" { + fields := strings.Fields(line) + if len(fields) == 2 { + count, _ := strconv.Atoi(fields[0]) //nolint + macCounts = append(macCounts, counting{ + Name: fields[1], + Count: count, + }) + + } + } + } + + totalValidShares := 0.0 + for _, ni := range nodesInfos { + share := 1.0 + for _, ipCount := range ipCounts { + if ni.IPAddr == ipCount.Name { + if ipCount.Count >= 8 { + share = 8.0 / float64(ipCount.Count) + } + } + } + for _, macCount := range macCounts { + if macCount.Name == ni.MacAddr { + share = share / float64(macCount.Count) + } + } + totalValidShares += share + } + + if !h0 { + fmt.Printf("Visors meeting uptime & architecture requirements: %d\n", len(nodesInfos)) + fmt.Printf("Unique mac addresses for first interface after lo: %d\n", len(uniqueMac)) + fmt.Printf("Unique Ip Addresses: %d\n", len(uniqueIP)) + fmt.Printf("Unique UUIDs: %d\n", len(uniqueUUID)) + fmt.Printf("Total valid shares: %.6f\n", totalValidShares) + fmt.Printf("Skycoin Per Share: %.6f\n", dayReward/totalValidShares) + } + for i, ni := range nodesInfos { + nodesInfos[i].Share = 1.0 + for _, ipCount := range ipCounts { + if ni.IPAddr == ipCount.Name { + if ipCount.Count >= 8 { + nodesInfos[i].Share = 8.0 / float64(ipCount.Count) + } + } + } + for _, macCount := range macCounts { + if macCount.Name == ni.MacAddr { + nodesInfos[i].Share = nodesInfos[i].Share / float64(macCount.Count) + } + } + nodesInfos[i].Reward = nodesInfos[i].Share * dayReward / float64(totalValidShares) + } + + if !h1 { + fmt.Println("Skycoin Address, Skywire Public Key, Reward Shares, Reward SKY Amount, IP, Architecture, UUID, Interfaces") + for _, ni := range nodesInfos { + fmt.Printf("%s, %s, %.6f, %.6f, %s, %s, %s, %s \n", ni.SkyAddr, ni.PK, ni.Share, ni.Reward, ni.IPAddr, ni.Arch, ni.UUID, ni.Interfaces) + } + } + rewardSumBySkyAddr := make(map[string]float64) + for _, ni := range nodesInfos { + rewardSumBySkyAddr[ni.SkyAddr] += ni.Reward + } + var sortedSkyAddrs []rewardData + for skyAddr, rewardSum := range rewardSumBySkyAddr { + sortedSkyAddrs = append(sortedSkyAddrs, rewardData{SkyAddr: skyAddr, Reward: rewardSum}) + } + sort.Slice(sortedSkyAddrs, func(i, j int) bool { + return sortedSkyAddrs[i].Reward > sortedSkyAddrs[j].Reward + }) + if !h0 { + fmt.Printf("Total Reward Amount: %.6f\n", func() (tr float64) { + for _, skyAddrReward := range sortedSkyAddrs { + tr += skyAddrReward.Reward + } + return tr + }()) + } + if !h2 { + fmt.Println("Skycoin Address, Reward Amount") + for _, skyAddrReward := range sortedSkyAddrs { + fmt.Printf("%s, %.6f\n", skyAddrReward.SkyAddr, skyAddrReward.Reward) + } + + } + }, +} diff --git a/cmd/skywire-cli/commands/root.go b/cmd/skywire-cli/commands/root.go index b474a1276..52a63ba05 100644 --- a/cmd/skywire-cli/commands/root.go +++ b/cmd/skywire-cli/commands/root.go @@ -4,10 +4,11 @@ package commands import ( "fmt" "log" + "os" + "path/filepath" "strings" "github.com/bitfield/script" - cc "github.com/ivanpirog/coloredcobra" "github.com/pterm/pterm" "github.com/pterm/pterm/putils" "github.com/spf13/cobra" @@ -15,13 +16,14 @@ import ( "github.com/skycoin/skywire-utilities/pkg/buildinfo" clicompletion "github.com/skycoin/skywire/cmd/skywire-cli/commands/completion" cliconfig "github.com/skycoin/skywire/cmd/skywire-cli/commands/config" - clidmsghttp "github.com/skycoin/skywire/cmd/skywire-cli/commands/dmsghttp" clidmsgpty "github.com/skycoin/skywire/cmd/skywire-cli/commands/dmsgpty" clilog "github.com/skycoin/skywire/cmd/skywire-cli/commands/log" climdisc "github.com/skycoin/skywire/cmd/skywire-cli/commands/mdisc" cliskysocksc "github.com/skycoin/skywire/cmd/skywire-cli/commands/proxy" clireward "github.com/skycoin/skywire/cmd/skywire-cli/commands/reward" + clirewards "github.com/skycoin/skywire/cmd/skywire-cli/commands/rewards" clirtfind "github.com/skycoin/skywire/cmd/skywire-cli/commands/rtfind" + clirtree "github.com/skycoin/skywire/cmd/skywire-cli/commands/rtree" cliskyfwd "github.com/skycoin/skywire/cmd/skywire-cli/commands/skyfwd" cliskyrev "github.com/skycoin/skywire/cmd/skywire-cli/commands/skyrev" clisurvey "github.com/skycoin/skywire/cmd/skywire-cli/commands/survey" @@ -31,9 +33,37 @@ import ( "github.com/skycoin/skywire/cmd/skywire-cli/internal" ) +func init() { + RootCmd.AddCommand( + cliconfig.RootCmd, + clidmsgpty.RootCmd, + clivisor.RootCmd, + clivpn.RootCmd, + cliut.RootCmd, + cliskyfwd.RootCmd, + cliskyrev.RootCmd, + clireward.RootCmd, + clirewards.RootCmd, + clisurvey.RootCmd, + clirtfind.RootCmd, + clirtree.RootCmd, + climdisc.RootCmd, + clicompletion.RootCmd, + clilog.RootCmd, + cliskysocksc.RootCmd, + treeCmd, + docCmd, + ) + var jsonOutput bool + RootCmd.PersistentFlags().BoolVar(&jsonOutput, internal.JSONString, false, "print output in json") + RootCmd.PersistentFlags().MarkHidden(internal.JSONString) //nolint +} + // RootCmd is the root command for skywire-cli var RootCmd = &cobra.Command{ - Use: "cli", + Use: func() string { + return strings.Split(filepath.Base(strings.ReplaceAll(strings.ReplaceAll(fmt.Sprintf("%v", os.Args), "[", ""), "]", "")), " ")[0] + }(), Short: "Command Line Interface for skywire", Long: ` ┌─┐┬┌─┬ ┬┬ ┬┬┬─┐┌─┐ ┌─┐┬ ┬ @@ -178,62 +208,9 @@ var docCmd = &cobra.Command{ }, } -func init() { - RootCmd.AddCommand( - cliconfig.RootCmd, - clidmsgpty.RootCmd, - clivisor.RootCmd, - clivpn.RootCmd, - cliut.RootCmd, - cliskyfwd.RootCmd, - cliskyrev.RootCmd, - clireward.RootCmd, - clisurvey.RootCmd, - clirtfind.RootCmd, - climdisc.RootCmd, - clicompletion.RootCmd, - clilog.RootCmd, - cliskysocksc.RootCmd, - treeCmd, - docCmd, - clidmsghttp.RootCmd, - ) - var jsonOutput bool - RootCmd.PersistentFlags().BoolVar(&jsonOutput, internal.JSONString, false, "print output in json") - RootCmd.PersistentFlags().MarkHidden(internal.JSONString) //nolint - var helpflag bool - RootCmd.SetUsageTemplate(help) - RootCmd.PersistentFlags().BoolVarP(&helpflag, "help", "h", false, "help for "+RootCmd.Use) - RootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) - RootCmd.PersistentFlags().MarkHidden("help") //nolint -} - // Execute executes root CLI command. func Execute() { - cc.Init(&cc.Config{ - RootCmd: RootCmd, - Headings: cc.HiBlue + cc.Bold, //+ cc.Underline, - Commands: cc.HiBlue + cc.Bold, - CmdShortDescr: cc.HiBlue, - Example: cc.HiBlue + cc.Italic, - ExecName: cc.HiBlue + cc.Bold, - Flags: cc.HiBlue + cc.Bold, - //FlagsDataType: cc.HiBlue, - FlagsDescr: cc.HiBlue, - NoExtraNewlines: true, - NoBottomNewline: true, - }) if err := RootCmd.Execute(); err != nil { log.Fatal("Failed to execute command: ", err) } } - -const help = "Usage:\r\n" + - " {{.UseLine}}{{if .HasAvailableSubCommands}}{{end}} {{if gt (len .Aliases) 0}}\r\n\r\n" + - "{{.NameAndAliases}}{{end}}{{if .HasAvailableSubCommands}}\r\n\r\n" + - "Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand)}}\r\n " + - "{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}\r\n\r\n" + - "Flags:\r\n" + - "{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}\r\n\r\n" + - "Global Flags:\r\n" + - "{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}\r\n\r\n" diff --git a/cmd/skywire-cli/commands/rtree/rtree.go b/cmd/skywire-cli/commands/rtree/rtree.go new file mode 100644 index 000000000..7c76e77d1 --- /dev/null +++ b/cmd/skywire-cli/commands/rtree/rtree.go @@ -0,0 +1,185 @@ +// Package clirtree subcommand for skywire-cli +package clirtree + +import ( + "fmt" + "os" + "strings" + + "github.com/bitfield/script" + "github.com/pterm/pterm" + "github.com/pterm/pterm/putils" + "github.com/spf13/cobra" + "github.com/tidwall/pretty" + + utilenv "github.com/skycoin/skywire-utilities/pkg/skyenv" + "github.com/skycoin/skywire/cmd/skywire-cli/internal" +) + +var ( + sortedEdgeKeys []string + utURL string + tpdURL string + cacheFileTPD string + cacheFileUT string + cacheFilesAge int + padSpaces int + isStats bool + rawData bool + refinedData bool + noFilterOnline bool +) + +// RootCmd is rtreeCmd +var RootCmd = rtreeCmd + +func init() { + rtreeCmd.Flags().StringVarP(&tpdURL, "tpdurl", "a", utilenv.TpDiscAddr, "transport discovery url") + rtreeCmd.Flags().StringVarP(&utURL, "uturl", "w", utilenv.UptimeTrackerAddr, "uptime tracker url") + rtreeCmd.Flags().BoolVarP(&rawData, "raw", "r", false, "print raw json data") + rtreeCmd.Flags().BoolVarP(&refinedData, "pretty", "p", false, "print pretty json data") + rtreeCmd.Flags().BoolVarP(&noFilterOnline, "noton", "o", false, "do not filter by online status in UT") + rtreeCmd.Flags().StringVar(&cacheFileTPD, "cft", os.TempDir()+"/tpd.json", "TPD cache file location") + rtreeCmd.Flags().StringVar(&cacheFileUT, "cfu", os.TempDir()+"/ut.json", "UT cache file location.") + rtreeCmd.Flags().IntVarP(&cacheFilesAge, "cfa", "m", 5, "update cache files if older than n minutes") + //TODO: calculate tree levels initially and apply appropriate padding ; as an alternative to manually padding + rtreeCmd.Flags().IntVarP(&padSpaces, "pad", "P", 15, "padding between tree and tpid") + rtreeCmd.Flags().BoolVarP(&isStats, "stats", "s", false, "return only statistics") +} + +var rtreeCmd = &cobra.Command{ + Use: "rtree", + Short: "map of transports on the skywire network", + Long: fmt.Sprintf("display a tree representation of transports from TPD\n\n%v/all-transports\n\nSet cache file location to \"\" to avoid using cache files", utilenv.TpDiscAddr), + Run: func(cmd *cobra.Command, args []string) { + tps := internal.GetData(cacheFileTPD, tpdURL+"/all-transports", cacheFilesAge) + if rawData { + script.Echo(tps).Stdout() //nolint + return + } + if refinedData { + script.Echo(string(pretty.Color(pretty.Pretty([]byte(tps)), nil))).Stdout() //nolint + return + } + var uts string + var utkeys []string + var offlinekeys []string + if !noFilterOnline { + uts = internal.GetData(cacheFileUT, utURL+"/uptimes?v=v2", cacheFilesAge) + utkeys, _ = script.Echo(uts).JQ(".[] | select(.on) | .pk").Replace("\"", "").Slice() //nolint + offlinekeys, _ = script.Echo(uts).JQ(".[] | select(.on | not) | .pk").Replace("\"", "").Slice() //nolint + } + + sortedEdgeKeys, _ = script.Echo(tps).JQ(".[].edges[]").Freq().Column(2).Slice() //nolint + + if isStats { + fmt.Printf("Unique keys in Transport Discovery: %d\n", len(sortedEdgeKeys)) + tpcount, _ := script.Echo(tps).JQ(".[].type").CountLines() //nolint + fmt.Printf("Count of transports: %v\n", tpcount) + tptypes, _ := script.Echo(tps).JQ(".[].type").Freq().String() //nolint + fmt.Printf("types of transports: \n%v\n", tptypes) + vcount, _ := script.Echo(tps).JQ(".[].edges[]").Freq().String() //nolint + fmt.Printf("Visors by transport count:\n%v\n", vcount) + return + } + + fmt.Printf("Tree *Online %s %s TPID Type\n", pterm.Black(pterm.BgRed.Sprint("*Offline")), pterm.Red("*Not in UT")) + + leveledList := pterm.LeveledList{} + edgeKey := sortedEdgeKeys[0] + leveledList = append(leveledList, pterm.LeveledListItem{Level: 0, Text: filterOnlineStatus(utkeys, offlinekeys, edgeKey)}) + + var usedkeys []string + usedkeys = append(usedkeys, edgeKey) + var lvl func(n int, k string) + lvl = func(n int, k string) { + l, _ := script.Echo(tps).JQ(".[] | select(.edges[] == " + k + ") | .edges[] | select(. != " + k + ")").Slice() //nolint + for _, m := range l { + if m == k { + continue + } + var ok bool + ok = false + for _, o := range usedkeys { + if m == o { + ok = true + } + } + if ok { + continue + } + usedkeys = append(usedkeys, m) + var tpid string + if n == 0 { + tpid = "" + } else { + tpid, _ = script.Echo(tps).JQ(".[] | select(.edges | index(" + k + ") and index(" + m + ")) | .t_id + \" \" + .type").First(1).String() //nolint + } + leveledList = append(leveledList, pterm.LeveledListItem{Level: n, Text: strings.ReplaceAll(strings.ReplaceAll(fmt.Sprintf("%s %s", filterOnlineStatus(utkeys, offlinekeys, m), strings.Repeat(" ", func() int { + indent := padSpaces - 4 - n*2 + if indent < 0 { + return 0 + } + return indent + }())+tpid), "\n", ""), "\"", "")}) + lvl(n+1, m) + } + } + lvl(1, edgeKey) + + pterm.DefaultTree.WithRoot(putils.TreeFromLeveledList(leveledList)).Render() //nolint + for _, edgeKey := range sortedEdgeKeys { + found := false + for _, usedKey := range usedkeys { + if usedKey == edgeKey { + found = true + break + } + } + if !found { + leveledList = pterm.LeveledList{} + leveledList = append(leveledList, pterm.LeveledListItem{Level: 0, Text: filterOnlineStatus(utkeys, offlinekeys, edgeKey)}) + usedkeys = append(usedkeys, edgeKey) + lvl(1, edgeKey) + pterm.DefaultTree.WithRoot(putils.TreeFromLeveledList(leveledList)).Render() //nolint + } + } + l, _ := script.Echo(tps).JQ(".[] | select(.edges[0] == .edges[1]) | .edges[0] + \""+strings.Repeat(" ", padSpaces)+"\" + .t_id + \" \" + .type").Replace("\"", "").Slice() //nolint + if len(l) > 0 { + pterm.Println(pterm.Red("Self-transports")) + for _, m := range l { + pterm.Println(filterOnlineStatus(utkeys, offlinekeys, m)) + } + } + }, +} + +func filterOnlineStatus(utkeys, offlinekeys []string, key string) (lvlN string) { + isOnline, isOffline := false, false + lvlN = strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(key, " ", ""), "\t", ""), "\n", ""), "\"", "") + if !noFilterOnline { + for _, k := range utkeys { + if strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(k, " ", ""), "\t", ""), "\n", ""), "\"", "") == strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(key, " ", ""), "\t", ""), "\n", ""), "\"", "") { + isOnline = true + break + } + } + if !isOnline { + for _, k := range offlinekeys { + if strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(k, " ", ""), "\t", ""), "\n", ""), "\"", "") == strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(key, " ", ""), "\t", ""), "\n", ""), "\"", "") { + isOffline = true + break + } + } + } + } else { + isOnline, isOffline = true, false + } + if !isOnline && !isOffline { + lvlN = pterm.Red(strings.ReplaceAll(key, "\"", "")) + } + if isOffline { + lvlN = pterm.Black(pterm.BgRed.Sprint(strings.ReplaceAll(key, "\"", ""))) + } + return lvlN +} diff --git a/cmd/skywire-cli/commands/ut/root.go b/cmd/skywire-cli/commands/ut/root.go index 732c76d03..1f7c1e0cb 100644 --- a/cmd/skywire-cli/commands/ut/root.go +++ b/cmd/skywire-cli/commands/ut/root.go @@ -2,204 +2,57 @@ package cliut import ( - "encoding/json" "fmt" - "io" - "log" - "net/http" "os" - "strconv" - "time" + "github.com/bitfield/script" "github.com/spf13/cobra" - "github.com/skycoin/dmsg/pkg/direct" - "github.com/skycoin/dmsg/pkg/disc" - "github.com/skycoin/dmsg/pkg/dmsg" - "github.com/skycoin/dmsg/pkg/dmsghttp" - - "github.com/skycoin/skywire-utilities/pkg/cipher" - "github.com/skycoin/skywire-utilities/pkg/logging" - clirpc "github.com/skycoin/skywire/cmd/skywire-cli/commands/rpc" + utilenv "github.com/skycoin/skywire-utilities/pkg/skyenv" "github.com/skycoin/skywire/cmd/skywire-cli/internal" ) +// RootCmd is utCmd +var RootCmd = utCmd + var ( - pubkey cipher.PubKey - pk string - thisPk string - online bool - isStats bool - url string - data []byte - dmsgAddr string - dmsgIP string - utDmsgAddr string + pk string + online bool + isStats bool + utURL string + cacheFileUT string + cacheFilesAge int ) var minUT int func init() { - RootCmd.Flags().StringVarP(&pk, "pk", "k", "", "check uptime for the specified key") - RootCmd.Flags().BoolVarP(&online, "on", "o", false, "list currently online visors") - RootCmd.Flags().BoolVarP(&isStats, "stats", "s", false, "count the number of results") - RootCmd.Flags().IntVarP(&minUT, "min", "n", 75, "list visors meeting minimum uptime") - RootCmd.Flags().StringVarP(&url, "url", "u", "http://ut.skywire.skycoin.com/uptimes?v=v2", "specify alternative uptime tracker url\ndefault: http://ut.skywire.skycoin.com/uptimes?v=v2") - RootCmd.Flags().StringVar(&dmsgAddr, "dmsgAddr", "030c83534af1041aee60c2f124b682a9d60c6421876db7c67fc83a73c5effdbd96", "specific dmsg server address for dmsghttp query") - RootCmd.Flags().StringVar(&dmsgIP, "dmsgIP", "188.121.99.59:8081", "specific dmsg server ip for dmsghttp query") - RootCmd.Flags().StringVar(&utDmsgAddr, "utDmsgAddr", "dmsg://022c424caa6239ba7d1d9d8f7dab56cd5ec6ae2ea9ad97bb94ad4b48f62a540d3f:80", "dmsg address of uptime tracker") + utCmd.Flags().StringVarP(&pk, "pk", "k", "", "check uptime for the specified key") + utCmd.Flags().BoolVarP(&online, "on", "o", false, "list currently online visors") + utCmd.Flags().BoolVarP(&isStats, "stats", "s", false, "count the number of results") + utCmd.Flags().IntVarP(&minUT, "min", "n", 75, "list visors meeting minimum uptime") + utCmd.Flags().StringVar(&cacheFileUT, "cfu", os.TempDir()+"/ut.json", "UT cache file location.") + utCmd.Flags().IntVarP(&cacheFilesAge, "cfa", "m", 5, "update cache files if older than n minutes") + utCmd.Flags().StringVarP(&utURL, "url", "u", utilenv.UptimeTrackerAddr, "specify alternative uptime tracker url") } -// RootCmd contains commands that interact with the skywire-visor -var RootCmd = &cobra.Command{ +var utCmd = &cobra.Command{ Use: "ut", Short: "query uptime tracker", - Long: "query uptime tracker\n Check local visor daily uptime percent with:\n skywire-cli ut -k $(skywire-cli visor pk)", + Long: fmt.Sprintf("query uptime tracker\n\n%v/uptimes?v=v2\n\nCheck local visor daily uptime percent with:\n skywire-cli ut -k $(skywire-cli visor pk)n\nSet cache file location to \"\" to avoid using cache files", utilenv.UptimeTrackerAddr), Run: func(cmd *cobra.Command, _ []string) { - // tyring to connect with running visor - rpcClient, err := clirpc.Client(cmd.Flags()) - if err != nil { - internal.PrintError(cmd.Flags(), fmt.Errorf("unable to create RPC client: %w", err)) - } else { - data, err = rpcClient.FetchUptimeTrackerData(pk) - if err != nil { - internal.PrintError(cmd.Flags(), fmt.Errorf("unable to fetch uptime tracker data from RPC client: %w", err)) - } - } - // no rpc, so trying to dmsghttp query - if len(data) == 0 { - data, err = dmsgHTTPQuery(cmd) - if err != nil { - internal.PrintError(cmd.Flags(), fmt.Errorf("unable to fetch uptime tracker data in dmsgHTTPQuery method: %w", err)) - } - } - // nor rpc and dmsghttp, trying to direct http query - if len(data) == 0 { - data, err = httpQuery(cmd) - if err != nil { - internal.PrintFatalError(cmd.Flags(), fmt.Errorf("unable to fetch uptime tracker data in httpQuery method: %w", err)) - } - } - now := time.Now() - startDate := time.Date(now.Year(), now.Month(), -1, 0, 0, 0, 0, now.Location()).Format("2006-01-02") - endDate := time.Date(now.Year(), now.Month()+1, 1, 0, 0, 0, 0, now.Location()).Add(-1 * time.Second).Format("2006-01-02") - uts := uptimes{} - jsonErr := json.Unmarshal(data, &uts) - if jsonErr != nil { - log.Fatal(jsonErr) - } - var msg []string - for _, j := range uts { - thisPk = j.Pk - if online { - if j.On { - msg = append(msg, fmt.Sprintf(thisPk+"\n")) - } - } else { - selectedDaily(j.Daily, startDate, endDate) - } - } + uts := internal.GetData(cacheFileUT, utURL+"/uptimes?v=v2", cacheFilesAge) if online { + utKeysOnline, _ := script.Echo(uts).JQ(".[] | select(.on) | .pk").Match(pk).Replace("\"", "").Slice() //nolint if isStats { - internal.PrintOutput(cmd.Flags(), fmt.Sprintf("%d visors online\n", len(msg)), fmt.Sprintf("%d visors online\n", len(msg))) - os.Exit(0) + internal.PrintOutput(cmd.Flags(), fmt.Sprintf("%d visors online\n", len(utKeysOnline)), fmt.Sprintf("%d visors online\n", len(utKeysOnline))) + return } - for _, i := range msg { - internal.PrintOutput(cmd.Flags(), i, i) + for _, i := range utKeysOnline { + internal.PrintOutput(cmd.Flags(), i+"\n", i+"\n") } + return } + script.Echo(uts).JQ(".[] | \"\\(.pk) \\(.daily | to_entries[] | select(.value | tonumber > "+fmt.Sprintf("%d", minUT)+") | \"\\(.key) \\(.value)\")\"").Match(pk).Replace("\"", "").Stdout() //nolint }, } - -func selectedDaily(data map[string]string, startDate, endDate string) { - for date, uptime := range data { - if date >= startDate && date <= endDate { - utfloat, err := strconv.ParseFloat(uptime, 64) - if err != nil { - log.Fatal(err) - } - if utfloat >= float64(minUT) { - fmt.Print(thisPk) - fmt.Print(" ") - fmt.Println(date, uptime) - } - } - } -} - -func dmsgHTTPQuery(cmd *cobra.Command) ([]byte, error) { - pk, sk := cipher.GenerateKeyPair() - var dmsgAddrPK cipher.PubKey - err := dmsgAddrPK.Set(dmsgAddr) - if err != nil { - return []byte{}, err - } - - servers := []*disc.Entry{{Server: &disc.Server{Address: dmsgIP}, Static: dmsgAddrPK}} - keys := cipher.PubKeys{pk} - - entries := direct.GetAllEntries(keys, servers) - dClient := direct.NewClient(entries, logging.NewMasterLogger().PackageLogger("ut_dmsgHTTPQuery")) - - dmsgDC, closeDmsgDC, err := direct.StartDmsg(cmd.Context(), logging.NewMasterLogger().PackageLogger("ut_dmsgHTTPQuery"), - pk, sk, dClient, dmsg.DefaultConfig()) - if err != nil { - return []byte{}, fmt.Errorf("failed to start dmsg: %w", err) - } - defer closeDmsgDC() - - dmsgHTTP := http.Client{Transport: dmsghttp.MakeHTTPTransport(cmd.Context(), dmsgDC)} - - resp, err := dmsgHTTP.Get(utDmsgAddr + "/uptimes?v=v2") - if err != nil { - return []byte{}, err - } - return io.ReadAll(resp.Body) -} - -func httpQuery(cmd *cobra.Command) ([]byte, error) { - if pk != "" { - err := pubkey.Set(pk) - if err != nil { - return []byte{}, err - } - url += "&visors=" + pubkey.String() - } - utClient := http.Client{ - Timeout: time.Second * 15, // Timeout after 15 seconds - } - - req, err := http.NewRequest(http.MethodGet, url, nil) - if err != nil { - return []byte{}, err - } - - res, err := utClient.Do(req) - if err != nil { - return []byte{}, err - } - - if res.Body != nil { - defer func() { - err := res.Body.Close() - if err != nil { - internal.PrintError(cmd.Flags(), fmt.Errorf("Failed to close response body")) - } - }() - } - - body, err := io.ReadAll(res.Body) - if err != nil { - return []byte{}, err - } - return body, nil -} - -type uptimes []struct { - Pk string `json:"pk"` - Up int `json:"up"` - Down int `json:"down"` - Pct float64 `json:"pct"` - On bool `json:"on"` - Daily map[string]string `json:"daily,omitempty"` -} diff --git a/cmd/skywire-cli/commands/visor/ip.go b/cmd/skywire-cli/commands/visor/ip.go index 024e11b66..7a6bee94f 100644 --- a/cmd/skywire-cli/commands/visor/ip.go +++ b/cmd/skywire-cli/commands/visor/ip.go @@ -39,10 +39,15 @@ var ipCmd = &cobra.Command{ func getIPAddress() (string, error) { var info ipInfo + var resp *http.Response + var err error - resp, err := http.Get("https://ip.skycoin.com/") + resp, err = http.Get("https://ip.skycoin.com/") if err != nil { - return info.IP, err + resp, err = http.Get("https://ip.plaintext.ir/") + if err != nil { + return info.IP, err + } } respBody, err := io.ReadAll(resp.Body) if err != nil { diff --git a/cmd/skywire-cli/commands/visor/transports.go b/cmd/skywire-cli/commands/visor/transports.go index b1b25a9ae..e0cec9460 100644 --- a/cmd/skywire-cli/commands/visor/transports.go +++ b/cmd/skywire-cli/commands/visor/transports.go @@ -203,25 +203,20 @@ var rmTpCmd = &cobra.Command{ Long: "\n Remove transport(s) by id", DisableFlagsInUseLine: true, Run: func(cmd *cobra.Command, args []string) { - //TODO - //if removeAll { - // var pks cipher.PubKeys - // internal.Catch(cmd.Flags(), pks.Set(strings.Join(filterPubKeys, ","))) - // tID, err := clirpc.Client(cmd.Flags()).Transports(filterTypes, pks, showLogs) - // internal.Catch(cmd.Flags(), err) - // internal.Catch(cmd.Flags(), clirpc.Client(cmd.Flags()).RemoveTransport(tID)) - //} else { - if args[0] != "" { - tpID = args[0] - } - tID := internal.ParseUUID(cmd.Flags(), "transport-id", tpID) rpcClient, err := clirpc.Client(cmd.Flags()) - if err != nil { - os.Exit(1) + if removeAll { + internal.Catch(cmd.Flags(), rpcClient.RemoveAllTransports()) + internal.PrintOutput(cmd.Flags(), "OK", "OK\n") + } else if tpID != "" { + tID := internal.ParseUUID(cmd.Flags(), "transport-id", tpID) + if err != nil { + os.Exit(1) + } + internal.Catch(cmd.Flags(), rpcClient.RemoveTransport(tID)) + internal.PrintOutput(cmd.Flags(), "OK", "OK\n") + } else { + internal.PrintOutput(cmd.Flags(), "", cmd.Help()) } - internal.Catch(cmd.Flags(), rpcClient.RemoveTransport(tID)) - internal.PrintOutput(cmd.Flags(), "OK", "OK\n") - //} }, } diff --git a/cmd/skywire-cli/commands/vpn/root.go b/cmd/skywire-cli/commands/vpn/root.go index 3fd553e5b..9f9e2e896 100644 --- a/cmd/skywire-cli/commands/vpn/root.go +++ b/cmd/skywire-cli/commands/vpn/root.go @@ -4,26 +4,31 @@ package clivpn import ( "github.com/spf13/cobra" + "github.com/skycoin/skywire-utilities/pkg/buildinfo" "github.com/skycoin/skywire-utilities/pkg/cipher" "github.com/skycoin/skywire/pkg/servicedisc" ) var ( - stateName = "vpn-client" - serviceType = servicedisc.ServiceTypeVPN - servicePort = ":3" - path string - isPkg bool - isUnFiltered bool - ver string - country string - isStats bool - pubkey cipher.PubKey - pk string - count int - sdURL string - directQuery bool - servers []servicedisc.Service + version = buildinfo.Version() + stateName = "vpn-client" + serviceType = servicedisc.ServiceTypeVPN + isUnFiltered bool + rawData bool + utURL string + sdURL string + cacheFileSD string + cacheFileUT string + cacheFilesAge int + noFilterOnline bool + path string + isPkg bool + ver string + country string + isStats bool + pubkey cipher.PubKey + pk string + startingTimeout int ) // RootCmd contains commands that interact with the skywire-visor diff --git a/cmd/skywire-cli/commands/vpn/vvpn.go b/cmd/skywire-cli/commands/vpn/vvpn.go index 9adca3d5b..eee5127d2 100644 --- a/cmd/skywire-cli/commands/vpn/vvpn.go +++ b/cmd/skywire-cli/commands/vpn/vvpn.go @@ -4,26 +4,22 @@ package clivpn import ( "bytes" "context" - "encoding/json" "fmt" - "math/rand" - "net/http" "os" "strings" "text/tabwriter" "time" + "github.com/bitfield/script" "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/spf13/pflag" + "github.com/tidwall/pretty" - "github.com/skycoin/skywire-utilities/pkg/buildinfo" "github.com/skycoin/skywire-utilities/pkg/cmdutil" "github.com/skycoin/skywire-utilities/pkg/skyenv" clirpc "github.com/skycoin/skywire/cmd/skywire-cli/commands/rpc" "github.com/skycoin/skywire/cmd/skywire-cli/internal" "github.com/skycoin/skywire/pkg/app/appserver" - "github.com/skycoin/skywire/pkg/servicedisc" "github.com/skycoin/skywire/pkg/visor" ) @@ -35,28 +31,8 @@ func init() { statusCmd, listCmd, ) - version := buildinfo.Version() - if version == "unknown" { - version = "" - } startCmd.Flags().StringVarP(&pk, "pk", "k", "", "server public key") - listCmd.Flags().StringVarP(&sdURL, "url", "a", "", "service discovery url default:\n"+skyenv.ServiceDiscAddr) - listCmd.Flags().BoolVarP(&directQuery, "direct", "b", false, "query service discovery directly") - listCmd.Flags().StringVarP(&pk, "pk", "k", "", "check "+serviceType+" service discovery for public key") - listCmd.Flags().IntVarP(&count, "num", "n", 0, "number of results to return") - listCmd.Flags().BoolVarP(&isUnFiltered, "unfilter", "u", false, "provide unfiltered results") - listCmd.Flags().StringVarP(&ver, "ver", "v", version, "filter results by version") - listCmd.Flags().StringVarP(&country, "country", "c", "", "filter results by country") - listCmd.Flags().BoolVarP(&isStats, "stats", "s", false, "return only a count of the results") -} - -func findServerByPK(servers []servicedisc.Service, addr string) *servicedisc.Service { - for _, server := range servers { - if server.Addr.String() == addr { - return &server - } - } - return nil + startCmd.Flags().IntVarP(&startingTimeout, "timeout", "t", 0, "starting timeout value in second") } var startCmd = &cobra.Command{ @@ -83,11 +59,16 @@ var startCmd = &cobra.Command{ } internal.Catch(cmd.Flags(), rpcClient.StartVPNClient(pubkey)) internal.PrintOutput(cmd.Flags(), nil, "Starting.") - ctx, cancel := cmdutil.SignalContext(context.Background(), &logrus.Logger{}) + tCtc := context.Background() //nolint + if startingTimeout != 0 { + tCtc, _ = context.WithTimeout(context.Background(), time.Duration(startingTimeout)*time.Second) //nolint + } + ctx, cancel := cmdutil.SignalContext(tCtc, &logrus.Logger{}) go func() { <-ctx.Done() cancel() - rpcClient.StopVPNClient("vpn-client") //nolint + rpcClient.KillApp("vpn-client") //nolint + fmt.Print("\nStopped!") os.Exit(1) }() startProcess := true @@ -181,113 +162,95 @@ var statusCmd = &cobra.Command{ }, } +var isLabel bool + +func init() { + if version == "unknown" { + version = "" + } + version = strings.Split(version, "-")[0] + listCmd.Flags().StringVarP(&utURL, "uturl", "w", skyenv.UptimeTrackerAddr, "uptime tracker url") + listCmd.Flags().StringVarP(&sdURL, "sdurl", "a", skyenv.ServiceDiscAddr, "service discovery url") + listCmd.Flags().BoolVarP(&rawData, "raw", "r", false, "print raw data") + listCmd.Flags().BoolVarP(&noFilterOnline, "noton", "o", false, "do not filter by online status in UT") + listCmd.Flags().StringVar(&cacheFileSD, "cfs", os.TempDir()+"/vpnsd.json", "SD cache file location") + listCmd.Flags().StringVar(&cacheFileUT, "cfu", os.TempDir()+"/ut.json", "UT cache file location.") + listCmd.Flags().IntVarP(&cacheFilesAge, "cfa", "m", 5, "update cache files if older than n minutes") + listCmd.Flags().StringVarP(&pk, "pk", "k", "", "check "+serviceType+" service discovery for public key") + listCmd.Flags().BoolVarP(&isUnFiltered, "unfilter", "u", false, "provide unfiltered results") + listCmd.Flags().StringVarP(&ver, "ver", "v", version, "filter results by version") + listCmd.Flags().StringVarP(&country, "country", "c", "", "filter results by country") + listCmd.Flags().BoolVarP(&isStats, "stats", "s", false, "return only a count of the results") + listCmd.Flags().BoolVarP(&isLabel, "label", "l", false, "label keys by country \033[91m(SLOW)\033[0m") +} + var listCmd = &cobra.Command{ Use: "list", - Short: "List " + serviceType + " servers", - Long: "List " + serviceType + " servers from service discovery\n " + skyenv.ServiceDiscAddr + "/api/services?type=" + serviceType + "\n " + skyenv.ServiceDiscAddr + "/api/services?type=" + serviceType + "&country=US", + Short: "List servers", + Long: fmt.Sprintf("List %v servers from service discovery\n%v/api/services?type=%v\n%v/api/services?type=%v&country=US\n\nSet cache file location to \"\" to avoid using cache files", serviceType, skyenv.ServiceDiscAddr, serviceType, skyenv.ServiceDiscAddr, serviceType), Run: func(cmd *cobra.Command, args []string) { - //validate any specified public key + sds := internal.GetData(cacheFileSD, sdURL+"/api/services?type="+serviceType, cacheFilesAge) + if rawData { + script.Echo(string(pretty.Color(pretty.Pretty([]byte(sds)), nil))).Stdout() //nolint + return + } if pk != "" { - err := pubkey.Set(pk) - if err != nil { - internal.PrintFatalError(cmd.Flags(), fmt.Errorf("Invalid or missing public key")) + if isStats { + count, _ := script.Echo(sds).JQ(`map(select(.address == "`+pk+`:3"))`).Replace("\"", "").Replace(":", " ").Column(1).CountLines() //nolint + script.Echo(fmt.Sprintf("%v\n", count)).Stdout() //nolint + return } + jsonOut, _ := script.Echo(sds).JQ(`map(select(.address == "` + pk + `:3"))`).Bytes() //nolint + script.Echo(string(pretty.Color(pretty.Pretty(jsonOut), nil))).Stdout() //nolint + return } - if sdURL == "" { - sdURL = skyenv.ServiceDiscAddr - } - if isUnFiltered { - ver = "" - country = "" + var sdJQ string + if !isUnFiltered { + if ver != "" && country == "" { + sdJQ = `select(.version == "` + ver + `")` + } + if country != "" && ver == "" { + sdJQ = `select(.geo.country == "` + country + `")` + } + if country != "" && ver != "" { + sdJQ = `select(.geo.country == "` + country + `" and .version == "` + ver + `")` + } } - if directQuery { - servers = directQuerySD(cmd.Flags()) + if sdJQ != "" { + sdJQ = `.[] | ` + sdJQ + ` | .address` } else { - rpcClient, err := clirpc.Client(cmd.Flags()) - if err != nil { - internal.PrintError(cmd.Flags(), fmt.Errorf("unable to create RPC client: %w", err)) - internal.PrintOutput(cmd.Flags(), fmt.Sprintf("directly querying service discovery\n%s/api/services?type=%s\n", sdURL, serviceType), fmt.Sprintf("directly querying service discovery\n%s/api/services?type=%s\n", sdURL, serviceType)) - servers = directQuerySD(cmd.Flags()) - } else { - servers, err = rpcClient.VPNServers(ver, country) - if err != nil { - internal.PrintError(cmd.Flags(), err) - internal.PrintOutput(cmd.Flags(), fmt.Sprintf("directly querying service discovery\n%s/api/services?type=%s\n", sdURL, serviceType), fmt.Sprintf("directly querying service discovery\n%s/api/services?type=%s\n", sdURL, serviceType)) - servers = directQuerySD(cmd.Flags()) - } - } + sdJQ = `.[] .address` } - if len(servers) == 0 { - internal.PrintOutput(cmd.Flags(), "No Servers found", "No Servers found") - os.Exit(0) + var sdkeys string + sdkeys, _ = script.Echo(sds).JQ(sdJQ).Replace("\"", "").Replace(":", " ").Column(1).String() //nolint + if noFilterOnline { + if isStats { + count, _ := script.Echo(sdkeys).CountLines() //nolint + script.Echo(fmt.Sprintf("%v\n", count)).Stdout() //nolint + return + } + script.Echo(sdkeys).Stdout() //nolint + return } + uts := internal.GetData(cacheFileUT, utURL+"/uptimes?v=v2", cacheFilesAge) + utkeys, _ := script.Echo(uts).JQ(".[] | select(.on) | .pk").Replace("\"", "").String() //nolint if isStats { - internal.PrintOutput(cmd.Flags(), fmt.Sprintf("%d Servers\n", len(servers)), fmt.Sprintf("%d Servers\n", len(servers))) + count, _ := script.Echo(sdkeys + utkeys).Freq().Match("2 ").Column(2).CountLines() //nolint + script.Echo(fmt.Sprintf("%v\n", count)).Stdout() //nolint + return + } + if !isLabel { + script.Echo(sdkeys + utkeys).Freq().Match("2 ").Column(2).Stdout() //nolint } else { - var msg string - var results []string - limit := len(servers) - if count > 0 && count < limit { - limit = count - } - if pk != "" { - for _, server := range servers { - if strings.Replace(server.Addr.String(), servicePort, "", 1) == pk { - results = append(results, server.Addr.String()) - } - } - } else { - for _, server := range servers { - results = append(results, server.Addr.String()) + filteredKeys, _ := script.Echo(sdkeys + utkeys).Freq().Match("2 ").Column(2).Slice() //nolint + formattedoutput, _ := script.Echo(sds).JQ(".[] | \"\\(.address) \\(.geo.country)\"").Replace("\"", "").Slice() //nolint + // Very slow! + for _, fo := range formattedoutput { + for _, fk := range filteredKeys { + script.Echo(fo).Match(fk).Stdout() //nolint } } - - //randomize the order of the displayed results - rand.Shuffle(len(results), func(i, j int) { - results[i], results[j] = results[j], results[i] - }) - for i := 0; i < limit && i < len(results); i++ { - msg += strings.Replace(results[i], servicePort, "", 1) - if server := findServerByPK(servers, results[i]); server != nil && server.Geo != nil { - if server.Geo.Country != "" { - msg += fmt.Sprintf(" | %s\n", server.Geo.Country) - } else { - msg += "\n" - } - } else { - msg += "\n" - } - } - internal.PrintOutput(cmd.Flags(), servers, msg) } - }, -} -func directQuerySD(cmdFlags *pflag.FlagSet) (s []servicedisc.Service) { - //url/uri format - //https://sd.skycoin.com/api/services?type=vpn&country=US&version=v1.3.7 - sdURL += "/api/services?type=" + serviceType - if country != "" { - sdURL += "&country=" + country - } - if ver != "" { - sdURL += "&version=" + ver - } - //preform http get request for the service discovery URL - resp, err := (&http.Client{Timeout: time.Duration(30 * time.Second)}).Get(sdURL) - if err != nil { - internal.PrintFatalError(cmdFlags, fmt.Errorf("error fetching servers from service discovery: %w", err)) - } - defer func() { - err := resp.Body.Close() - if err != nil { - internal.PrintError(cmdFlags, fmt.Errorf("Failed to close response body")) - } - }() - // Decode JSON response into struct - err = json.NewDecoder(resp.Body).Decode(&s) - if err != nil { - internal.PrintFatalError(cmdFlags, fmt.Errorf("error decoding json to struct: %w", err)) - } - return s + }, } diff --git a/cmd/skywire-cli/internal/internal.go b/cmd/skywire-cli/internal/internal.go index 2c8c6d3c8..ab5098697 100644 --- a/cmd/skywire-cli/internal/internal.go +++ b/cmd/skywire-cli/internal/internal.go @@ -2,10 +2,14 @@ package internal import ( + "bytes" "encoding/json" "fmt" + "net/http" "os" + "time" + "github.com/bitfield/script" "github.com/google/uuid" "github.com/spf13/pflag" @@ -113,3 +117,32 @@ func PrintOutput(cmdFlags *pflag.FlagSet, outputJSON, output interface{}) { fmt.Print(output) } } + +// GetData fetches data from the specified URL via http or from cached file +func GetData(cachefile, thisurl string, cacheFilesAge int) (thisdata string) { + var shouldfetch bool + buf1 := new(bytes.Buffer) + cTime := time.Now() + if cachefile == "" { + thisdata, _ = script.NewPipe().WithHTTPClient(&http.Client{Timeout: 30 * time.Second}).Get(thisurl).String() //nolint + return thisdata + } + if cachefile != "" { + if u, err := os.Stat(cachefile); err != nil { + shouldfetch = true + } else { + if cTime.Sub(u.ModTime()).Minutes() > float64(cacheFilesAge) { + shouldfetch = true + } + } + if shouldfetch { + _, _ = script.NewPipe().WithHTTPClient(&http.Client{Timeout: 30 * time.Second}).Get(thisurl).Tee(buf1).WriteFile(cachefile) //nolint + thisdata = buf1.String() + } else { + thisdata, _ = script.File(cachefile).String() //nolint + } + } else { + thisdata, _ = script.NewPipe().WithHTTPClient(&http.Client{Timeout: 30 * time.Second}).Get(thisurl).String() //nolint + } + return thisdata +} diff --git a/cmd/skywire-cli/skywire-cli.go b/cmd/skywire-cli/skywire-cli.go index e9ad4310f..013177566 100644 --- a/cmd/skywire-cli/skywire-cli.go +++ b/cmd/skywire-cli/skywire-cli.go @@ -2,9 +2,42 @@ package main import ( + cc "github.com/ivanpirog/coloredcobra" + "github.com/spf13/cobra" + "github.com/skycoin/skywire/cmd/skywire-cli/commands" ) +func init() { + var helpflag bool + commands.RootCmd.SetUsageTemplate(help) + commands.RootCmd.PersistentFlags().BoolVarP(&helpflag, "help", "h", false, "help menu") + commands.RootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) + commands.RootCmd.PersistentFlags().MarkHidden("help") //nolint +} + func main() { + cc.Init(&cc.Config{ + RootCmd: commands.RootCmd, + Headings: cc.HiBlue + cc.Bold, + Commands: cc.HiBlue + cc.Bold, + CmdShortDescr: cc.HiBlue, + Example: cc.HiBlue + cc.Italic, + ExecName: cc.HiBlue + cc.Bold, + Flags: cc.HiBlue + cc.Bold, + FlagsDescr: cc.HiBlue, + NoExtraNewlines: true, + NoBottomNewline: true, + }) commands.Execute() } + +const help = "Usage:\r\n" + + " {{.UseLine}}{{if .HasAvailableSubCommands}}{{end}} {{if gt (len .Aliases) 0}}\r\n\r\n" + + "{{.NameAndAliases}}{{end}}{{if .HasAvailableSubCommands}}\r\n\r\n" + + "Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand)}}\r\n " + + "{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}\r\n\r\n" + + "Flags:\r\n" + + "{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}\r\n\r\n" + + "Global Flags:\r\n" + + "{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}\r\n\r\n" diff --git a/cmd/skywire-systray/README.md b/cmd/skywire-systray/README.md deleted file mode 100644 index 691b7b3ce..000000000 --- a/cmd/skywire-systray/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# CLI Documentation - -skywire command line interface - - - -- [Install](#install) -- [skywire-systray usage](#skywire-systray-usage) - - - -## Install - -The skywire-systray interacts with the skywire-visor via skywire-cli via the default shell, and additionally interacts with with the scripts and services or batch files included with the skywire installation provided by the linux and mac packages or the windows .msi installer. - -A desktop environment is required for the skywire-systray - -```bash -$ cd $GOPATH/src/github.com/skycoin/skywire/cmd/skywire-systray -$ go install ./... -``` - -## skywire-systray usage - -After the installation, you can run `skywire-systray` to see the usage: - -``` -$ skywire-systray -skywire systray - -Usage: - skywire-systray [flags] - -Flags: - -s, --src 'go run' using the skywire sources - -d, --dev show remote visors & dmsghttp ui - -h, --help help for skywire-systray - -``` - -![skywire-systray](https://user-images.githubusercontent.com/36607567/184662776-d16f0660-9a05-4e4d-b769-5f17735f9644.png) - -The skywire-systray can control the running state of the visor. - -The linux implementation can update the visor's config via `skywire-autoconfig` when the visor is shut down. This will also start the visor - -![skywire-systray1](https://user-images.githubusercontent.com/36607567/184664444-9b08a5ee-2e39-445d-8f7a-352d83fea777.png) diff --git a/cmd/skywire-systray/icons/icon.ico b/cmd/skywire-systray/icons/icon.ico deleted file mode 100644 index a59afb5cf..000000000 Binary files a/cmd/skywire-systray/icons/icon.ico and /dev/null differ diff --git a/cmd/skywire-systray/icons/icon.png b/cmd/skywire-systray/icons/icon.png deleted file mode 100644 index fddafbe9a..000000000 Binary files a/cmd/skywire-systray/icons/icon.png and /dev/null differ diff --git a/cmd/skywire-systray/icons/icon.tiff b/cmd/skywire-systray/icons/icon.tiff deleted file mode 100644 index b635a4967..000000000 Binary files a/cmd/skywire-systray/icons/icon.tiff and /dev/null differ diff --git a/cmd/skywire-systray/skywire-systray.go b/cmd/skywire-systray/skywire-systray.go deleted file mode 100644 index 41fc9a450..000000000 --- a/cmd/skywire-systray/skywire-systray.go +++ /dev/null @@ -1,431 +0,0 @@ -// /* cmd/skywire-systray/skywire-systray.go -/* -skywire systray -*/ -package main - -import ( - "embed" - "fmt" - "log" - "strings" - "sync" - "time" - - "github.com/bitfield/script" - cc "github.com/ivanpirog/coloredcobra" - "github.com/skycoin/skycoin/src/util/logging" - "github.com/skycoin/systray" - "github.com/spf13/cobra" - - "github.com/skycoin/skywire/pkg/visor/visorconfig" -) - -var ( - isSourcerun bool - isDevrun bool - remotevisors []string - vpnserverpks []string - skywirecli string - mHV *systray.MenuItem - mVisors *systray.MenuItem - mVPN *systray.MenuItem - mVPNButton *systray.MenuItem - mVPNClient *systray.MenuItem - mVPNStatus *systray.MenuItem - mVPNUI *systray.MenuItem //nolint:unused - mPTY *systray.MenuItem - mShutdown *systray.MenuItem - mStart *systray.MenuItem - mAutoconfig *systray.MenuItem - mQuit *systray.MenuItem - mRemoteVisors []*systray.MenuItem - mVPNServers []*systray.MenuItem - servers []*systray.MenuItem //nolint - l *logging.MasterLogger - vpnStatusMx sync.Mutex - err error -) - -func init() { - l = logging.NewMasterLogger() - //disable sorting, flags appear in the order shown here - rootCmd.Flags().SortFlags = false - rootCmd.Flags().BoolVarP(&isSourcerun, "src", "s", false, "'go run' using the skywire sources") - rootCmd.Flags().BoolVarP(&isDevrun, "dev", "d", false, "show remote visors & dmsghttp ui") - -} - -var rootCmd = &cobra.Command{ - Use: "skywire-systray", - Short: "skywire systray", - SilenceErrors: true, - SilenceUsage: true, - DisableSuggestions: true, - // PreRun: func(cmd *cobra.Command, _ []string) { - // }, - Run: func(cmd *cobra.Command, args []string) { - //skywire-cli command to use - if !isSourcerun { - skywirecli = "skywire-cli" - } else { - skywirecli = "go run cmd/skywire-cli/skywire-cli.go" - } - onExit := func() { - now := time.Now() - fmt.Println("Exit at", now.String()) - } - systray.Run(onReady, onExit) - }, -} - -// Execute executes root command. -func Execute() { - cc.Init(&cc.Config{ - RootCmd: rootCmd, - Headings: cc.HiBlue + cc.Bold, //+ cc.Underline, - Commands: cc.HiBlue + cc.Bold, - CmdShortDescr: cc.HiBlue, - Example: cc.HiBlue + cc.Italic, - ExecName: cc.HiBlue + cc.Bold, - Flags: cc.HiBlue + cc.Bold, - //FlagsDataType: cc.HiBlue, - FlagsDescr: cc.HiBlue, - NoExtraNewlines: true, - NoBottomNewline: true, - }) - if err = rootCmd.Execute(); err != nil { - log.Fatal("Failed to execute command: ", err) - } -} - -//go:embed icons/* -var iconFS embed.FS - -func main() { - Execute() -} - -func onReady() { - l := logging.NewMasterLogger() - sysTrayIcon, err := ReadSysTrayIcon() - if err != nil { - l.WithError(err).Fatalln("Failed to read system tray icon") - } - systray.SetTemplateIcon(sysTrayIcon, sysTrayIcon) - systray.SetTitle("Skywire") - systray.SetTooltip("Skywire") - mQuit = systray.AddMenuItem("Quit", "Quit the whole app") - - //check that the visor is running and responds over RPC - visor, err := script.Exec(skywirecli + ` visor pk`).Match("FATAL").String() - if err != nil { - l.WithError(err).Warn("Failed to get visor public key") - //visor should be empty string if the visor is running - visor = " " - } - systray.SetTemplateIcon(sysTrayIcon, sysTrayIcon) - systray.SetTitle("Skywire") - - //Top level menu - //mHV launches the hypervisor with `skywire-cli hv ui` - mHV = systray.AddMenuItem("Hypervisor", "Hypervisor") - mHV.Hide() - //mPTY launches the dmsgpty ui with `skywire-cli hv dmsg ui` - mPTY = systray.AddMenuItem("DMSGPTY UI", "DMSGPTY UI") - mPTY.Hide() - //mVPNUI launches the VPN ui with `skywire-cli hv dmsg ui` - mVPNUI = systray.AddMenuItem("VPN UI", "VPN UI") - mVPNUI.Hide() - //mVisors menu to access dmsgpty ui for connected remote visors - mVisors = systray.AddMenuItem("Visors", "Visors") - mVisors.Hide() - //mVPNClient contains the vpn menu and server list submenu - mVPNClient = systray.AddMenuItem("VPN", "VPN Client Submenu") - mVPNClient.Hide() - //mStart start a stopped the visor - mStart = systray.AddMenuItem("Start", "Start") - mStart.Hide() - //mAutoconfig run the autoconfig script provided by the package or installer - mAutoconfig = systray.AddMenuItem("Autoconfig", "Autoconfig") - mAutoconfig.Hide() - //mShutdown shut down a running visor - mShutdown = systray.AddMenuItem("Shutdown", "Shutdown") - mShutdown.Hide() - - //Sub menus - //mVPNStatus shows current VPN connection status derived from `skywire-cli visor app info` - mVPNStatus = mVPNClient.AddSubMenuItem("Status: Disconnected", "VPN Client Status") - mVPNStatus.Disable() - //mVPNButton VPN on / off button - mVPNButton = mVPNClient.AddSubMenuItem("Connect", "VPN Client Switch Button") - //mVPN is the list of VPN server public keys returned by `skywire-cli hv vpn list` - mVPN = mVPNClient.AddSubMenuItem("VPN Servers", "VPN Servers") - - if visor != "" { - ToggleOff() - } else { - if isDevrun { - //check for connected visors - visors, err := script.Exec(skywirecli + ` dmsgpty list`).String() - if err != nil { - l.WithError(err).Warn("Failed to fetch connected visors " + visors) - } - remotevisors = strings.Split(visors, "\n") - for i := range remotevisors { - if remotevisors[i] != "" { - l.Info("remote visors: " + remotevisors[i]) - } - } - mRemoteVisors = []*systray.MenuItem{} - for _, v := range remotevisors { - if v != "" { - mRemoteVisors = append(mRemoteVisors, mVisors.AddSubMenuItem(v, "")) - } - } - go visorsBtn(mRemoteVisors) - } - go vpnStatusBtn() - //check for available vpn servers - vpnlistpks, err := script.Exec(skywirecli + ` vpn list -y`).String() - if err != nil { - l.WithError(err).Warn("Failed to fetch vpn servers") - } - vpnlistpks = strings.Trim(vpnlistpks, "[") - vpnlistpks = strings.Trim(vpnlistpks, "]") - vpnserverpks = strings.Split(vpnlistpks, "\n") - mVPNServers = []*systray.MenuItem{} - for _, v := range vpnserverpks { - if v != "" { - mVPNServers = append(mVPNServers, mVPN.AddSubMenuItemCheckbox(v, "", false)) - } - } - go serversBtn(mVPNServers) - ToggleOn() - } - systray.AddSeparator() - //this blank item retains minimum text displacement - - go func() { - <-mQuit.ClickedCh - fmt.Println("Requesting quit") - systray.Quit() - fmt.Println("Finished quitting") - }() - go func() { - for { - select { - case <-mHV.ClickedCh: - _, err = script.Exec(skywirecli + ` visor hvui`).Stdout() - if err != nil { - l.WithError(err).Warn("Failed to open hypervisor UI") - } - case <-mVPNUI.ClickedCh: - _, err = script.Exec(skywirecli + ` vpn ui`).Stdout() - if err != nil { - l.WithError(err).Warn("Failed to open VPN UI") - } - case <-mVPNButton.ClickedCh: - handleVPNButton() - case <-mPTY.ClickedCh: - _, err = script.Exec(skywirecli + ` dmsg ui`).Stdout() - if err != nil { - l.WithError(err).Warn("Failed to open dmsgpty UI") - } - case <-mStart.ClickedCh: - _, err = script.Exec(`systemctl enable --now skywire`).Stdout() - if err != nil { - l.WithError(err).Warn("Failed to start skywire") - } else { - ToggleOn() - } - case <-mAutoconfig.ClickedCh: - //execute the skywire-autoconfig script includedwith the skywire package - _, err = script.Exec(`exo-open --launch TerminalEmulator bash -c 'sudo SKYBIAN=true skywire-autoconfig && sleep 5'`).Stdout() - if err != nil { - l.WithError(err).Warn("Failed to generate skywire configuration") - } else { - ToggleOn() - } - case <-mShutdown.ClickedCh: - if visorconfig.OS == "linux" { - _, _ = script.Exec(`systemctl disable --now skywire`).Stdout() //nolint:errcheck - ToggleOff() - } else { - l.Warn("shutdown of services not yet implemented on windows / mac") - } - _, err = script.Exec(skywirecli + ` visor halt 2> /dev/null`).Stdout() - if err != nil { - l.WithError(err).Warn("Failed to stop skywire") - } else { - ToggleOff() - } - case <-mQuit.ClickedCh: - systray.Quit() - fmt.Println("Quit2 now...") - return - } - } - }() -} - -// ReadSysTrayIcon reads system tray icon. -func ReadSysTrayIcon() (contents []byte, err error) { - contents, err = iconFS.ReadFile("icons/icon.png") - if err != nil { - err = fmt.Errorf("failed to read icon: %w", err) - } - return contents, err -} - -func visorsBtn(mRemoteVisors []*systray.MenuItem) { - btnChannel := make(chan int) - for index, remotevisor := range mRemoteVisors { - go func(chn chan int, remotevisor *systray.MenuItem, index int) { - for { //nolint - select { - case <-remotevisor.ClickedCh: - l.Info("opening dmsgpty ui to visor: " + remotevisors[index]) - _, err = script.Exec(skywirecli + ` hv dmsg ui -v ` + remotevisors[index]).Stdout() - if err != nil { - l.WithError(err).Warn("Failed to open dmsgpty UI") - } - chn <- index - } - } - }(btnChannel, remotevisor, index) - } -} - -func serversBtn(servers []*systray.MenuItem) { //nolint - btnChannel := make(chan int) - for index, server := range servers { //nolint - go func(chn chan int, server *systray.MenuItem, index int) { - for { //nolint - select { - case <-server.ClickedCh: - chn <- index - } - } - }(btnChannel, server, index) - } - - for { - selectedServer := servers[<-btnChannel] - serverTempValue := strings.Split(selectedServer.String(), ",")[2] - serverPK := serverTempValue[2 : len(serverTempValue)-7] - for _, server := range servers { //nolint - server.Uncheck() - server.Enable() - } - selectedServer.Check() - selectedServer.Disable() - // pk := cipher.PubKey{} - // if err := pk.UnmarshalText([]byte(serverPK)); err != nil { - // continue - // } - stats, err := script.Exec(skywirecli + ` vpn status`).String() - if err != nil { - break - } - if stats == "running\n" { - _, err = script.Exec(skywirecli + ` vpn stop`).Stdout() - if err != nil { - l.WithError(err).Warn("Failed to stop vpn-client") - } - } - _, err = script.Exec(`bash -c 'export VPNSERVERPK=` + serverPK + ` ; ` + skywirecli + ` vpn start ${VPNSERVERPK%% *}'`).Stdout() - if err != nil { - l.WithError(err).Warn("Failed to start vpn-client") - } - } -} - -func vpnStatusBtn() { - for { - vpnStatusMx.Lock() - stats, err := script.Exec(skywirecli + ` vpn status`).String() - if err != nil { - mVPNStatus.SetTitle("Status: Disconnected") - mVPNButton.SetTitle("Connect") - break - } - if stats == "running\n" { - mVPNStatus.SetTitle("Status: Connected") - mVPNButton.SetTitle("Disconnect") - } - if stats == "stopped\n" { - mVPNStatus.SetTitle("Status: Disconnected") - mVPNButton.SetTitle("Connect") - } - if stats == "error\n" { - mVPNStatus.SetTitle("Status: Error") - mVPNButton.SetTitle("Connect") - - } - vpnStatusMx.Unlock() - time.Sleep(2 * time.Second) - } -} - -func handleVPNButton() { //nolint - appstate, err := script.Exec(skywirecli + ` vpn status`).String() - if err != nil { - l.WithError(err).Warn("Failed to get vpn-client status") - } - if appstate == "running\n" { - _, err = script.Exec(skywirecli + ` vpn stop `).Stdout() - if err != nil { - l.WithError(err).Warn("Failed to stop vpn-client") - } - } else { - _, err = script.Exec(skywirecli + ` vpn start `).Stdout() - if err != nil { - l.WithError(err).Warn("Failed to start vpn-client") - } - } -} - -// ToggleOn menu when skywire visor is running -func ToggleOn() { - //check for connected visors - visors, err := script.Exec(skywirecli + ` dmsgpty list`).String() - if err != nil { - l.WithError(err).Warn("Failed to fetch connected visors " + visors) - } - if isDevrun { - mPTY.Show() - if (visors != "") && (visors != "\n") { - mVisors.Show() - } else { - mVisors.Hide() - } - } else { - mVisors.Hide() - mPTY.Hide() - } - mHV.Show() - mVPNUI.Show() - mVPN.Show() - mVPNClient.Show() - mStart.Hide() - mAutoconfig.Hide() - mShutdown.Show() - mQuit.Show() -} - -// ToggleOff menu when skywire visor is NOT running -func ToggleOff() { - mHV.Hide() - mPTY.Hide() - mVPNUI.Hide() - mVPNClient.Hide() - mVisors.Hide() - mShutdown.Hide() - - mStart.Show() - if visorconfig.OS == "linux" { - mAutoconfig.Show() - } - mQuit.Show() -} diff --git a/cmd/skywire-visor/skywire-visor.go b/cmd/skywire-visor/skywire-visor.go index b0f8af39a..b24cf074c 100644 --- a/cmd/skywire-visor/skywire-visor.go +++ b/cmd/skywire-visor/skywire-visor.go @@ -1,20 +1,27 @@ // /* cmd/skywire/skywire.go /* -skywire +skywire-visor */ package main import ( - "fmt" - cc "github.com/ivanpirog/coloredcobra" + "github.com/spf13/cobra" - "github.com/skycoin/skywire/pkg/visor" + commands "github.com/skycoin/skywire/pkg/visor" ) +func init() { + var helpflag bool + commands.RootCmd.SetUsageTemplate(help) + commands.RootCmd.PersistentFlags().BoolVarP(&helpflag, "help", "h", false, "help menu") + commands.RootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) + commands.RootCmd.PersistentFlags().MarkHidden("help") //nolint +} + func main() { cc.Init(&cc.Config{ - RootCmd: visor.RootCmd, + RootCmd: commands.RootCmd, Headings: cc.HiBlue + cc.Bold, Commands: cc.HiBlue + cc.Bold, CmdShortDescr: cc.HiBlue, @@ -25,8 +32,15 @@ func main() { NoExtraNewlines: true, NoBottomNewline: true, }) - - if err := visor.RootCmd.Execute(); err != nil { - fmt.Println(err) - } + commands.Execute() } + +const help = "Usage:\r\n" + + " {{.UseLine}}{{if .HasAvailableSubCommands}}{{end}} {{if gt (len .Aliases) 0}}\r\n\r\n" + + "{{.NameAndAliases}}{{end}}{{if .HasAvailableSubCommands}}\r\n\r\n" + + "Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand)}}\r\n " + + "{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}\r\n\r\n" + + "Flags:\r\n" + + "{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}\r\n\r\n" + + "Global Flags:\r\n" + + "{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}\r\n\r\n" diff --git a/cmd/skywire/README.md b/cmd/skywire/README.md new file mode 100644 index 000000000..6fb04b769 --- /dev/null +++ b/cmd/skywire/README.md @@ -0,0 +1,2974 @@ +# Skywire Merged Binary + + +# skywire documentation + +## subcommand tree + +A tree representation of the skywire subcommands + +``` +└─┬skywire + ├──visor + ├─┬cli + │ ├─┬config + │ │ ├──gen + │ │ ├──gen-keys + │ │ ├──check-pk + │ │ └─┬update + │ │ ├──dmsghttp + │ │ ├──svc + │ │ ├──hv + │ │ ├──sc + │ │ ├──ss + │ │ ├──vpnc + │ │ └──vpns + │ ├─┬dmsgpty + │ │ ├──ui + │ │ ├──url + │ │ ├──list + │ │ └──start + │ ├─┬visor + │ │ ├─┬app + │ │ │ ├──ls + │ │ │ ├──start + │ │ │ ├──stop + │ │ │ ├──register + │ │ │ ├──deregister + │ │ │ ├──log + │ │ │ └─┬arg + │ │ │ ├──autostart + │ │ │ ├──killswitch + │ │ │ ├──secure + │ │ │ ├──passcode + │ │ │ └──netifc + │ │ ├─┬hv + │ │ │ ├──ui + │ │ │ ├──cpk + │ │ │ └──pk + │ │ ├──pk + │ │ ├──info + │ │ ├──ver + │ │ ├──ports + │ │ ├──ip + │ │ ├──ping + │ │ ├──test + │ │ ├──start + │ │ ├──reload + │ │ ├──halt + │ │ ├─┬route + │ │ │ ├──ls-rules + │ │ │ ├──rule + │ │ │ ├──rm-rule + │ │ │ └─┬add-rule + │ │ │ ├──app + │ │ │ ├──fwd + │ │ │ └──intfwd + │ │ └─┬tp + │ │ ├──type + │ │ ├──ls + │ │ ├──id + │ │ ├──add + │ │ ├──rm + │ │ └──disc + │ ├─┬vpn + │ │ ├──start + │ │ ├──stop + │ │ ├──status + │ │ ├──list + │ │ ├──ui + │ │ └──url + │ ├──ut + │ ├──fwd + │ ├──rev + │ ├──reward + │ ├──rewards + │ ├──survey + │ ├──rtfind + │ ├──rtree + │ ├─┬mdisc + │ │ ├──entry + │ │ └──servers + │ ├──completion + │ ├──log + │ ├─┬proxy + │ │ ├──start + │ │ ├──stop + │ │ ├──status + │ │ └──list + │ ├──tree + │ └──doc + ├─┬svc + │ ├──sn + │ ├──tpd + │ ├──tps + │ ├──ar + │ ├──rf + │ ├──cb + │ ├──kg + │ ├──lc + │ ├──nv + │ ├─┬se + │ │ ├──visor + │ │ ├──dmsg + │ │ └──setup + │ ├──sd + │ ├──nwmon + │ ├──pvm + │ ├──ssm + │ └──vpnm + ├─┬dmsg + │ ├─┬pty + │ │ ├─┬cli + │ │ │ ├──whitelist + │ │ │ ├──whitelist-add + │ │ │ └──whitelist-remove + │ │ ├─┬host + │ │ │ └──confgen + │ │ └──ui + │ ├──disc + │ ├─┬server + │ │ ├─┬config + │ │ │ └──gen + │ │ └──start + │ ├──http + │ ├──curl + │ ├─┬web + │ │ └──gen-keys + │ ├─┬socks + │ │ ├──server + │ │ └──client + │ └──mon + ├─┬app + │ ├──vpn-server + │ ├──vpn-client + │ ├──skysocks-client + │ ├──skysocks + │ └──skychat + ├──tree + └──doc + + + +``` + +### visor + +``` + + ┌─┐┬┌─┬ ┬┬ ┬┬┬─┐┌─┐ ┬ ┬┬┌─┐┌─┐┬─┐ + └─┐├┴┐└┬┘││││├┬┘├┤───└┐┌┘│└─┐│ │├┬┘ + └─┘┴ ┴ ┴ └┴┘┴┴└─└─┘ └┘ ┴└─┘└─┘┴└─ + + + +Flags: + -c, --config string config file to use (default): skywire-config.json + -C, --confarg string supply config as argument + -b, --browser open hypervisor ui in default web browser + --systray run as systray + -i, --hvui run as hypervisor * + --all show all flags + --csrf Request a CSRF token for sensitive hypervisor API requests (default true) + + +``` + +### cli + +``` + + ┌─┐┬┌─┬ ┬┬ ┬┬┬─┐┌─┐ ┌─┐┬ ┬ + └─┐├┴┐└┬┘││││├┬┘├┤───│ │ │ + └─┘┴ ┴ ┴ └┴┘┴┴└─└─┘ └─┘┴─┘┴ + +Available Commands: + config Generate or update a skywire config + dmsgpty Interact with remote visors + visor Query the Skywire Visor + vpn VPN client + ut query uptime tracker + fwd Control skyforwarding + rev reverse proxy skyfwd + reward skycoin reward address + rewards calculate rewards from uptime data & collected surveys + survey system survey + rtfind Query the Route Finder + rtree map of transports on the skywire network + mdisc Query remote DMSG Discovery + completion Generate completion script + log survey & transport log collection + proxy Skysocks client + tree subcommand tree + doc generate markdown docs + + +``` + +skywire command line interface + +## skywire + +``` + + ┌─┐┬┌─┬ ┬┬ ┬┬┬─┐┌─┐ + └─┐├┴┐└┬┘││││├┬┘├┤ + └─┘┴ ┴ ┴ └┴┘┴┴└─└─┘ + +Available Commands: + visor Skywire Visor + cli Command Line Interface for skywire + svc Skywire services + dmsg Dmsg services & utilities + app skywire native applications + tree subcommand tree + doc generate markdown docs + + +``` + +## global flags + +The skywire-cli interacts with the running visor via rpc calls. By default the rpc server is available on localhost:3435. The rpc address and port the visor is using may be changed in the config file, once generated. + +It is not recommended to expose the rpc server on the local network. Exposing the rpc allows unsecured access to the machine over the local network + +``` + +Global Flags: + + --rpc string RPC server address (default "localhost:3435") + + --json bool print output as json + +``` + +#### cli config + +``` +Generate or update the config file used by skywire-visor. + +Available Commands: + gen Generate a config file + gen-keys generate public / secret keypair + check-pk check a skywire public key + update Update a config file + + +``` + +##### cli config gen + +``` +Generate a config file + + Config defaults file may also be specified with: + SKYENV=/path/to/skywire.conf skywire-cli config gen + print the SKYENV file template with: + skywire-cli config gen -q + + + +Flags: + -a, --url string services conf url + + (default "http://conf.skywire.skycoin.com") + --loglvl string level of logging in config (default "info") + -b, --bestproto best protocol (dmsg | direct) based on location + -c, --noauth disable authentication for hypervisor UI + -d, --dmsghttp use dmsg connection to skywire services + -D, --dmsgconf string dmsghttp-config path (default "dmsghttp-config.json") + --minsess int number of dmsg servers to connect to (0 = unlimited) (default 2) + -e, --auth enable auth on hypervisor UI + -f, --force remove pre-existing config + -g, --disableapps string comma separated list of apps to disable + -i, --ishv local hypervisor configuration + -j, --hvpks string list of public keys to add as hypervisor + --dmsgpty string add dmsgpty whitelist PKs + --survey string add survey whitelist PKs + --routesetup string add route setup node PKs + --tpsetup string add transport setup node PKs + -k, --os string (linux / mac / win) paths (default "linux") + -l, --publicip allow display node ip in services + -m, --example-apps add example apps to the config + -n, --stdout write config to stdout + -N, --squash output config without whitespace or newlines + -q, --envs show the environmental variable settings + -o, --out string output config: skywire-config.json + -p, --pkg use path for package: /opt/skywire + -u, --user use paths for user space: /home/d0mo + -r, --regen re-generate existing config & retain keys + -s, --sk cipher.SecKey a random key is generated if unspecified + + (default 0000000000000000000000000000000000000000000000000000000000000000) + -t, --testenv use test deployment conf.skywire.dev + -v, --servevpn enable vpn server + -w, --hide dont print the config to the terminal :: show errors with -n flag + -x, --retainhv retain existing hypervisors with regen + -y, --autoconn disable autoconnect to public visors + -z, --public publicize visor in service discovery + --stcpr int set tcp transport listening port - 0 for random + --sudph int set udp transport listening port - 0 for random + --binpath string set bin_path for visor vative apps + --proxyclientpk string set server public key for proxy client + --startproxyclient autostart proxy client + --noproxyserver disable autostart of proxy server + --proxyserverpass string set proxy server password + --proxyclientpass string password for the proxy client to access the server (if needed) + --killsw string vpn client killswitch + --addvpn string set vpn server public key for vpn client + --vpnpass string password for vpn client to access the vpn server (if needed) + --vpnserverpass string set password to the vpn server + --secure string change secure mode status of vpn server + --netifc string VPN Server network interface (detected: eno1) + --nofetch do not fetch the services from the service conf url + -S, --svcconf string fallback service configuration file (default "services-config.json") + --nodefaults do not use hardcoded defaults for production / test services + --version string custom version testing override + --all show all flags + + +``` + +##### Example for package / msi + +``` +$ skywire cli config gen -bpirxn +{ + "version": "v1.3.18", + "sk": "eab215b4851fb14cbcb856a0b763923bb0d21dde0ede41eeb7ff176327fe760a", + "pk": "03603bdd732230acfbbeaf769a92487b469176ff84d5cce1041bf36963cbbc1d69", + "dmsg": { + "discovery": "http://dmsgd.skywire.skycoin.com", + "sessions_count": 2, + "servers": [], + "servers_type": "all" + }, + "dmsgpty": { + "dmsg_port": 22, + "cli_network": "unix", + "cli_address": "/tmp/dmsgpty.sock", + "whitelist": [] + }, + "skywire-tcp": { + "pk_table": null, + "listening_address": ":7777" + }, + "transport": { + "discovery": "http://tpd.skywire.skycoin.com", + "address_resolver": "http://ar.skywire.skycoin.com", + "public_autoconnect": true, + "transport_setup": [ + "03530b786c670fc7f5ab9021478c7ec9cd06a03f3ea1416c50c4a8889ef5bba80e", + "03271c0de223b80400d9bd4b7722b536a245eb6c9c3176781ee41e7bac8f9bad21", + "03a792e6d960c88c6fb2184ee4f16714c58b55f0746840617a19f7dd6e021699d9", + "0313efedc579f57f05d4f5bc3fbf0261f31e51cdcfde7e568169acf92c78868926", + "025c7bbf23e3441a36d7e8a1e9d717921e2a49a2ce035680fec4808a048d244c8a", + "030eb6967f6e23e81db0d214f925fc5ce3371e1b059fb8379ae3eb1edfc95e0b46", + "02e582c0a5e5563aad47f561b272e4c3a9f7ac716258b58e58eb50afd83c286a7f", + "02ddc6c749d6ed067bb68df19c9bcb1a58b7587464043b1707398ffa26a9746b26", + "03aa0b1c4e23616872058c11c6efba777c130a85eaf909945d697399a1eb08426d", + "03adb2c924987d8deef04d02bd95236c5ae172fe5dfe7273e0461d96bf4bc220be" + ], + "log_store": { + "type": "file", + "location": "/opt/skywire/local/transport_logs", + "rotation_interval": "168h0m0s" + }, + "stcpr_port": 0, + "sudph_port": 0 + }, + "routing": { + "route_setup_nodes": [ + "0324579f003e6b4048bae2def4365e634d8e0e3054a20fc7af49daf2a179658557", + "024fbd3997d4260f731b01abcfce60b8967a6d4c6a11d1008812810ea1437ce438", + "03b87c282f6e9f70d97aeea90b07cf09864a235ef718725632d067873431dd1015" + ], + "route_finder": "http://rf.skywire.skycoin.com", + "route_finder_timeout": "10s", + "min_hops": 0 + }, + "uptime_tracker": { + "addr": "http://ut.skywire.skycoin.com" + }, + "launcher": { + "service_discovery": "http://sd.skycoin.com", + "apps": [ + { + "name": "vpn-client", + "binary": "vpn-client", + "args": [ + "--dns", + "1.1.1.1" + ], + "auto_start": false, + "port": 43 + }, + { + "name": "skychat", + "binary": "skychat", + "args": [ + "--addr", + ":8001" + ], + "auto_start": true, + "port": 1 + }, + { + "name": "skysocks", + "binary": "skysocks", + "auto_start": true, + "port": 3 + }, + { + "name": "skysocks-client", + "binary": "skysocks-client", + "args": [ + "--addr", + ":1080" + ], + "auto_start": false, + "port": 13 + }, + { + "name": "vpn-server", + "binary": "vpn-server", + "auto_start": false, + "port": 44 + } + ], + "server_addr": "localhost:5505", + "bin_path": "/opt/skywire/apps", + "display_node_ip": false + }, + "survey_whitelist": [ + "02b5ee5333aa6b7f5fc623b7d5f35f505cb7f974e98a70751cf41962f84c8c4637", + "03714c8bdaee0fb48f47babbc47c33e1880752b6620317c9d56b30f3b0ff58a9c3", + "020d35bbaf0a5abc8ec0ba33cde219fde734c63e7202098e1f9a6cf9daaeee55a9", + "027f7dec979482f418f01dfabddbd750ad036c579a16422125dd9a313eaa59c8e1", + "031d4cf1b7ab4c789b56c769f2888e4a61c778dfa5fe7e5cd0217fc41660b2eb65", + "0327e2cf1d2e516ecbfdbd616a87489cc92a73af97335d5c8c29eafb5d8882264a", + "03abbb3eff140cf3dce468b3fa5a28c80fa02c6703d7b952be6faaf2050990ebf4" + ], + "hypervisors": [], + "cli_addr": "localhost:3435", + "log_level": "", + "local_path": "/opt/skywire/local", + "dmsghttp_server_path": "/opt/skywire/local/custom", + "stun_servers": [ + "192.53.117.238:3478", + "170.187.228.44:3478", + "192.53.117.237:3478", + "192.53.117.146:3478", + "192.53.117.60:3478", + "192.53.117.124:3478", + "170.187.228.178:3478", + "170.187.225.246:3478" + ], + "shutdown_timeout": "10s", + "is_public": false, + "persistent_transports": null, + "hypervisor": { + "db_path": "/opt/skywire/users.db", + "enable_auth": true, + "cookies": { + "hash_key": "19a47254be4a7d9ce7664d20b4271bb402434eadfbb6c94dd59922d5cbf89ce3c03f1d54c320ca624fa44e8d85ad0b1df2a84acf607ef1ef7ea63bce99a50c53", + "block_key": "09df61d626fbda1632c91604620ca94c926125a109c4cf2f3d9bb608bd24b904", + "expires_duration": 43200000000000, + "path": "/", + "domain": "" + }, + "dmsg_port": 46, + "http_addr": ":8000", + "enable_tls": false, + "tls_cert_file": "./ssl/cert.pem", + "tls_key_file": "./ssl/key.pem" + } +} +``` + +##### cli config gen-keys + +``` +generate public / secret keypair + + + + +``` + +##### cli config check-pk + +``` +check a skywire public key + + + + +``` + +##### cli config update + +``` +Update a config file + +Available Commands: + dmsghttp update dmsghttp-config.json file from config bootstrap service + svc update services-config.json file from config bootstrap service + hv update hypervisor config + sc update skysocks-client config + ss update skysocks-server config + vpnc update vpn-client config + vpns update vpn-server config + +Flags: + -a, --endpoints update server endpoints + --log-level string level of logging in config + -b, --url string service config URL: conf.skywire.skycoin.com + -t, --testenv use test deployment: conf.skywire.dev + --public-autoconn string change public autoconnect configuration + --set-minhop int change min hops value (default -1) + -i, --input string path of input config file. + -o, --output string config file to output + -u, --user update config at: $HOME/skywire-config.json + + +``` + +###### cli config update dmsghttp + +``` +update dmsghttp-config.json file from config bootstrap service + + + +Flags: + -p, --path string path of dmsghttp-config file, default is for pkg installation (default "/opt/skywire/dmsghttp-config.json") + +Global Flags: + -i, --input string path of input config file. + -o, --output string config file to output + -u, --user update config at: $HOME/skywire-config.json + + +``` + +###### cli config update svc + +``` +update services-config.json file from config bootstrap service + + + +Flags: + -p, --path string path of services-config file, default is for pkg installation (default "/opt/skywire/services-config.json") + +Global Flags: + -i, --input string path of input config file. + -o, --output string config file to output + -u, --user update config at: $HOME/skywire-config.json + + +``` + +###### cli config update hv + +``` +update hypervisor config + + + +Flags: + -+, --add-pks string public keys of hypervisors that should be added to this visor + -r, --reset resets hypervisor configuration + +Global Flags: + -i, --input string path of input config file. + -o, --output string config file to output + -u, --user update config at: $HOME/skywire-config.json + + +``` + +###### cli config update sc + +``` +update skysocks-client config + + + +Flags: + -+, --add-server string add skysocks server address to skysock-client + -r, --reset reset skysocks-client configuration + +Global Flags: + -i, --input string path of input config file. + -o, --output string config file to output + -u, --user update config at: $HOME/skywire-config.json + + +``` + +###### cli config update ss + +``` +update skysocks-server config + + + +Flags: + -s, --passwd string add passcode to skysocks server + -r, --reset reset skysocks configuration + +Global Flags: + -i, --input string path of input config file. + -o, --output string config file to output + -u, --user update config at: $HOME/skywire-config.json + + +``` + +###### cli config update vpnc + +``` +update vpn-client config + + + +Flags: + -x, --killsw string change killswitch status of vpn-client + --add-server string add server address to vpn-client + -s, --pass string add passcode of server if needed + -r, --reset reset vpn-client configurations + +Global Flags: + -i, --input string path of input config file. + -o, --output string config file to output + -u, --user update config at: $HOME/skywire-config.json + + +``` + +###### cli config update vpns + +``` +update vpn-server config + + + +Flags: + -s, --passwd string add passcode to vpn-server + --secure string change secure mode status of vpn-server + --autostart string change autostart of vpn-server + --netifc string set default network interface + -r, --reset reset vpn-server configurations + +Global Flags: + -i, --input string path of input config file. + -o, --output string config file to output + -u, --user update config at: $HOME/skywire-config.json + + +``` + +#### cli dmsgpty + +``` +Interact with remote visors + +Available Commands: + ui Open dmsgpty UI in default browser + url Show dmsgpty UI URL + list List connected visors + start Start dmsgpty session + + +``` + +##### cli dmsgpty ui + +``` +Open dmsgpty UI in default browser + + + +Flags: + -i, --input string read from specified config file + -p, --pkg read from /opt/skywire/skywire.json + -v, --visor string public key of visor to connect to + + +``` + +##### cli dmsgpty url + +``` +Show dmsgpty UI URL + + + +Flags: + -i, --input string read from specified config file + -p, --pkg read from /opt/skywire/skywire.json + -v, --visor string public key of visor to connect to + + +``` + +##### cli dmsgpty list + +``` +List connected visors + + + +Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +##### cli dmsgpty start + +``` +Start dmsgpty session + + + +Flags: + -p, --port string port of remote visor dmsgpty (default "22") + --rpc string RPC server address (default "localhost:3435") + + +``` + +#### cli visor + +``` +Query the Skywire Visor + +Available Commands: + app App settings + hv Hypervisor + pk Public key of the visor + info Summary of visor info + ver Version and build info + ports List of Ports + ip IP information of network + ping Ping the visor with given pk + test Test the visor with public visors on network + start start visor + halt Stop a running visor + route View and set rules + tp View and set transports + +Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +##### cli visor app + +``` + + App settings + +Available Commands: + ls List apps + start Launch app + stop Halt app + register Register app + deregister Deregister app + log Logs from app + arg App args + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +###### cli visor app ls + +``` + + List apps + + + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +###### cli visor app start + +``` + + Launch app + + + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +###### cli visor app stop + +``` + + Halt app + + + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +###### cli visor app register + +``` + + Register app + + + +Flags: + -a, --appname string name of the app + -p, --localpath string path of the local folder (default "./local") + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +###### cli visor app deregister + +``` + + Deregister app + + + +Flags: + -k, --procKey string proc key of the app to deregister + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +###### cli visor app log + +``` + + Logs from app since RFC3339Nano-formatted timestamp. + + + "beginning" is a special timestamp to fetch all the logs + + + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +###### cli visor app arg + +``` +App args + +Available Commands: + autostart Set app autostart + killswitch Set app killswitch + secure Set app secure + passcode Set app passcode + netifc Set app network interface + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +###### cli visor app arg autostart + +``` +App args + +Available Commands: + autostart Set app autostart + killswitch Set app killswitch + secure Set app secure + passcode Set app passcode + netifc Set app network interface + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +###### cli visor app arg killswitch + +``` +App args + +Available Commands: + autostart Set app autostart + killswitch Set app killswitch + secure Set app secure + passcode Set app passcode + netifc Set app network interface + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +###### cli visor app arg secure + +``` +App args + +Available Commands: + autostart Set app autostart + killswitch Set app killswitch + secure Set app secure + passcode Set app passcode + netifc Set app network interface + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +###### cli visor app arg passcode + +``` +App args + +Available Commands: + autostart Set app autostart + killswitch Set app killswitch + secure Set app secure + passcode Set app passcode + netifc Set app network interface + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +###### cli visor app arg netifc + +``` +App args + +Available Commands: + autostart Set app autostart + killswitch Set app killswitch + secure Set app secure + passcode Set app passcode + netifc Set app network interface + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +##### cli visor hv + +``` + + Hypervisor + + + Access the hypervisor UI + + View remote hypervisor public key + +Available Commands: + ui open Hypervisor UI in default browser + cpk Public key of remote hypervisor(s) set in config + pk Public key of remote hypervisor(s) + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +###### cli visor hv ui + +``` + + open Hypervisor UI in default browser + + + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +###### cli visor hv cpk + +``` + + Public key of remote hypervisor(s) set in config + + + +Flags: + -w, --http serve public key via http + -i, --input string path of input config file. + -p, --pkg read from /opt/skywire/skywire.json + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +###### cli visor hv pk + +``` +Public key of remote hypervisor(s) which are currently connected to + + + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +##### cli visor pk + +``` + + Public key of the visor + + + +Flags: + -w, --http serve public key via http + -i, --input string path of input config file. + -p, --pkg read from {/opt/skywire/apps /opt/skywire/local {/opt/skywire/users.db true}} + -x, --prt string serve public key via http (default "7998") + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +##### cli visor info + +``` + + Summary of visor info + + + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +##### cli visor ver + +``` + + Version and build info + + + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +##### cli visor ports + +``` + + List of all ports used by visor services and apps + + + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +##### cli visor ip + +``` + + IP information of network + + + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +##### cli visor ping + +``` + + Creates a route with the provided pk as a hop and returns latency on the conn + + + +Flags: + -s, --size int Size of packet, in KB, default is 2KB (default 2) + -t, --tries int Number of tries (default 1) + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +##### cli visor test + +``` + + Creates a route with public visors as a hop and returns latency on the conn + + + +Flags: + -c, --count int Count of Public Visors for using in test. (default 2) + -s, --size int Size of packet, in KB, default is 2KB (default 2) + -t, --tries int Number of tries per public visors (default 1) + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +##### cli visor start + +``` +start visor + + + +Flags: + -s, --src 'go run' external commands from the skywire sources + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +##### cli visor reload + +``` +reload visor + + + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +##### cli visor halt + +``` + + Stop a running visor + + + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +##### cli visor route + +``` + + View and set routing rules + +Available Commands: + ls-rules List routing rules + rule Return routing rule by route ID key + rm-rule Remove routing rule + add-rule Add routing rule + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +###### cli visor route ls-rules + +``` + + List routing rules + + + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +###### cli visor route rule + +``` + + Return routing rule by route ID key + + + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +###### cli visor route rm-rule + +``` + + Remove routing rule + + + +Flags: + -a, --all remove all routing rules + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +###### cli visor route add-rule + +``` + + Add routing rule + +Available Commands: + app Add app/consume routing rule + fwd Add forward routing rule + intfwd Add intermediary forward routing rule + +Flags: + --keep-alive duration timeout for rule expiration (default 30s) + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +###### cli visor route add-rule app + +``` + + Add routing rule + +Available Commands: + app Add app/consume routing rule + fwd Add forward routing rule + intfwd Add intermediary forward routing rule + +Flags: + --keep-alive duration timeout for rule expiration (default 30s) + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +###### cli visor route add-rule fwd + +``` + + Add routing rule + +Available Commands: + app Add app/consume routing rule + fwd Add forward routing rule + intfwd Add intermediary forward routing rule + +Flags: + --keep-alive duration timeout for rule expiration (default 30s) + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +###### cli visor route add-rule intfwd + +``` + + Add routing rule + +Available Commands: + app Add app/consume routing rule + fwd Add forward routing rule + intfwd Add intermediary forward routing rule + +Flags: + --keep-alive duration timeout for rule expiration (default 30s) + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +##### cli visor tp + +``` + + Transports are bidirectional communication protocols + used between two Skywire Visors (or Transport Edges) + + Each Transport is represented as a unique 16 byte (128 bit) + UUID value called the Transport ID + and has a Transport Type that identifies + a specific implementation of the Transport. + + Types: stcp stcpr sudph dmsg + +Available Commands: + type Transport types used by the local visor + ls Available transports + id Transport summary by id + add Add a transport + rm Remove transport(s) by id + disc Discover remote transport(s) + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +###### cli visor tp type + +``` + + Transport types used by the local visor + + + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +###### cli visor tp ls + +``` + + Available transports + + displays transports of the local visor + + + +Flags: + -t, --types strings show transport(s) type(s) comma-separated + -p, --pks strings show transport(s) for public key(s) comma-separated + -l, --logs show transport logs (default true) + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +###### cli visor tp id + +``` + + Transport summary by id + + + +Flags: + -i, --id string transport ID + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +###### cli visor tp add + +``` + + Add a transport + + If the transport type is unspecified, + the visor will attempt to establish a transport + in the following order: skywire-tcp, stcpr, sudph, dmsg + + + +Flags: + -r, --rpk string remote public key. + -o, --timeout duration if specified, sets an operation timeout + -t, --type string type of transport to add. + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +###### cli visor tp rm + +``` + + Remove transport(s) by id + + + +Flags: + -a, --all remove all transports + -i, --id string remove transport of given ID + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +###### cli visor tp disc + +``` + + Discover remote transport(s) by ID or public key + + + +Flags: + -i, --id string obtain transport of given ID + -p, --pk string obtain transports by public key + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +#### cli vpn + +``` +VPN client + +Available Commands: + start start the vpn for + stop stop the vpnclient + status vpn client status + list List servers + ui Open VPN UI in default browser + url Show VPN UI URL + +Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +##### cli vpn start + +``` +start the vpn for + + + +Flags: + -k, --pk string server public key + -t, --timeout int starting timeout value in second (default 20) + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +##### cli vpn stop + +``` +stop the vpnclient + + + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +##### cli vpn status + +``` +vpn client status + + + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +##### cli vpn list + +``` +List vpn servers from service discovery +http://sd.skycoin.com/api/services?type=vpn +http://sd.skycoin.com/api/services?type=vpn&country=US + +Set cache file location to "" to avoid using cache files + + + +Flags: + -m, --cfa int update cache files if older than n minutes (default 5) + --cfs string SD cache file location (default "/tmp/vpnsd.json") + --cfu string UT cache file location. (default "/tmp/ut.json") + -c, --country string filter results by country + -l, --label label keys by country (SLOW) + -o, --noton do not filter by online status in UT + -k, --pk string check vpn service discovery for public key + -r, --raw print raw data + -a, --sdurl string service discovery url (default "http://sd.skycoin.com") + -s, --stats return only a count of the results + -u, --unfilter provide unfiltered results + -w, --uturl string uptime tracker url (default "http://ut.skywire.skycoin.com") + -v, --ver string filter results by version + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +##### cli vpn ui + +``` +Open VPN UI in default browser + + + +Flags: + -c, --config string config path + -p, --pkg use package config path: /opt/skywire + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +##### cli vpn url + +``` +Show VPN UI URL + + + +Flags: + -c, --config string config path + -p, --pkg use package config path: /opt/skywire + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +#### cli ut + +``` +query uptime tracker + +http://ut.skywire.skycoin.com/uptimes?v=v2 + +Check local visor daily uptime percent with: + skywire-cli ut -k $(skywire-cli visor pk)n +Set cache file location to "" to avoid using cache files + + + +Flags: + -m, --cfa int update cache files if older than n minutes (default 5) + --cfu string UT cache file location. (default "/tmp/ut.json") + -n, --min int list visors meeting minimum uptime (default 75) + -o, --on list currently online visors + -k, --pk string check uptime for the specified key + -s, --stats count the number of results + -u, --url string specify alternative uptime tracker url (default "http://ut.skywire.skycoin.com") + + +``` + +#### cli fwd + +``` +Control skyforwarding + forward local ports over skywire + + + +Flags: + -d, --deregister deregister local port of the external (http) app + -l, --ls list registered local ports + -p, --port int local port of the external (http) app + + +``` + +#### cli rev + +``` +connect or disconnect from remote ports + + + +Flags: + -l, --ls list configured connections + -k, --pk string remote public key to connect to + -p, --port int local port to reverse proxy + -r, --remote int remote port to read from + -d, --stop string disconnect from specified + + +``` + +#### cli reward + +``` + + skycoin reward address set to: + + + +Flags: + --all show all flags + + +``` + +#### cli rewards + +``` + +Collect surveys: skywire-cli log +Fetch uptimes: skywire-cli ut > ut.txt + + + +Flags: + -d, --date string date for which to calculate reward (default "2024-03-12") + -k, --pk string check reward for pubkey + -n, --noarch string disallowed architectures, comma separated (default "amd64") + -y, --year int yearly total rewards (default 408000) + -u, --utfile string uptime tracker data file (default "ut.txt") + -p, --path string path to the surveys (default "log_collecting") + -0, --h0 hide statistical data + -1, --h1 hide survey csv data + -2, --h2 hide reward csv data + -e, --err account for non rewarded keys + + +``` + +#### cli survey + +``` +print the system survey + + + +Flags: + -s, --sha generate checksum of system survey + + +``` + +``` +unknown command "survey" for "skywire" + +``` + +#### cli rtfind + +``` +Query the Route Finder +Assumes the local visor public key as an argument if only one argument is given + + + +Flags: + -n, --min uint16 minimum hops (default 1) + -x, --max uint16 maximum hops (default 1000) + -t, --timeout duration request timeout (default 10s) + -a, --addr string route finder service address + http://rf.skywire.skycoin.com + + +``` + +#### cli rtree + +``` +display a tree representation of transports from TPD + +http://tpd.skywire.skycoin.com/all-transports + +Set cache file location to "" to avoid using cache files + + + +Flags: + -m, --cfa int update cache files if older than n minutes (default 5) + --cft string TPD cache file location (default "/tmp/tpd.json") + --cfu string UT cache file location. (default "/tmp/ut.json") + -o, --noton do not filter by online status in UT + -P, --pad int padding between tree and tpid (default 15) + -p, --pretty print pretty json data + -r, --raw print raw json data + -s, --stats return only statistics + -a, --tpdurl string transport discovery url (default "http://tpd.skywire.skycoin.com") + -w, --uturl string uptime tracker url (default "http://ut.skywire.skycoin.com") + + +``` + +#### cli mdisc + +``` +Query remote DMSG Discovery + +Available Commands: + entry Fetch an entry + servers Fetch available servers + + +``` + +##### cli mdisc entry + +``` +Fetch an entry + + + +Flags: + -a, --addr string DMSG discovery server address + http://dmsgd.skywire.skycoin.com + + +``` + +##### cli mdisc servers + +``` +Fetch available servers + + + +Flags: + --addr string address of DMSG discovery server + (default "http://dmsgd.skywire.skycoin.com") + + +``` + +#### cli completion + +``` +Generate completion script + + + + +``` + +#### cli log + +``` +Fetch health, survey, and transport logging from visors which are online in the uptime tracker +http://ut.skywire.skycoin.com/uptimes?v=v2 +http://ut.skywire.skycoin.com/uptimes?v=v2&visors=;; + + + +Flags: + -e, --env string deployment to get uptimes from (default "prod") + -l, --log fetch only transport logs + -v, --survey fetch only surveys + -f, --file string fetch only a specific file from all online visors + -k, --pks string fetch only from specific public keys ; semicolon separated + -d, --dir string save files to specified dir (default "log_collecting") + -c, --clean delete files and folders on errors + --minv string minimum visor version to fetch from (default "v1.3.15") + --include-versions string list of version that not satisfy our minimum version condition, but we want include them + -n, --duration int number of days before today to fetch transport logs for + --all consider all visors ; no version filtering + --batchSize int number of visor in each batch (default 50) + --maxfilesize int maximum file size allowed to download during collecting logs, in KB (default 1024) + -D, --dmsg-disc string dmsg discovery url + (default "http://dmsgd.skywire.skycoin.com") + -u, --ut string custom uptime tracker url + -s, --sk cipher.SecKey a random key is generated if unspecified + + (default 0000000000000000000000000000000000000000000000000000000000000000) + + +``` + +#### cli proxy + +``` +Skysocks client + +Available Commands: + start start the proxy client + stop stop the proxy client + status proxy client status + list List servers + +Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +##### cli proxy start + +``` +start the proxy client + + + +Flags: + -a, --addr string address of proxy for use + -n, --name string name of skysocks client + -k, --pk string server public key + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +##### cli proxy stop + +``` +stop the proxy client + + + +Flags: + --all stop all skysocks client + --name string specific skysocks client that want stop + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +##### cli proxy status + +``` +proxy client status + + + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +##### cli proxy list + +``` +List proxy servers from service discovery +http://sd.skycoin.com/api/services?type=proxy +http://sd.skycoin.com/api/services?type=proxy&country=US + +Set cache file location to "" to avoid using cache files + + + +Flags: + -m, --cfa int update cache files if older than n minutes (default 5) + --cfs string SD cache file location (default "/tmp/proxysd.json") + --cfu string UT cache file location. (default "/tmp/ut.json") + -c, --country string filter results by country + -l, --label label keys by country (SLOW) + -o, --noton do not filter by online status in UT + -k, --pk string check proxy service discovery for public key + -r, --raw print raw data + -a, --sdurl string service discovery url (default "http://sd.skycoin.com") + -s, --stats return only a count of the results + -u, --unfilter provide unfiltered results + -w, --uturl string uptime tracker url (default "http://ut.skywire.skycoin.com") + -v, --ver string filter results by version + +Global Flags: + --rpc string RPC server address (default "localhost:3435") + + +``` + +#### cli tree + +``` +subcommand tree + + + + +``` + +#### cli doc + +``` +generate markdown docs + + UNHIDEFLAGS=1 go run cmd/skywire-cli/skywire-cli.go doc + + UNHIDEFLAGS=1 go run cmd/skywire-cli/skywire-cli.go doc > cmd/skywire-cli/README1.md + + generate toc: + + cat cmd/skywire-cli/README1.md | gh-md-toc + + + + +``` + +### svc + +``` + + ┌─┐┬┌─┬ ┬┬ ┬┬┬─┐┌─┐ ┌─┐┌─┐┬─┐┬ ┬┬┌─┐┌─┐┌─┐ + └─┐├┴┐└┬┘││││├┬┘├┤───└─┐├┤ ├┬┘└┐┌┘││ ├┤ └─┐ + └─┘┴ ┴ ┴ └┴┘┴┴└─└─┘ └─┘└─┘┴└─ └┘ ┴└─┘└─┘└─┘ + +Available Commands: + sn Route Setup Node for skywire + tpd Transport Discovery Server for skywire + tps Transport setup server for skywire + ar Address Resolver Server for skywire + rf Route Finder Server for skywire + cb Config Bootstrap Server for skywire + kg skywire keys generator, prints pub-key and sec-key + lc Liveness checker of the deployment. + nv Node Visualizer Server for skywire + se skywire environment generator + sd Service discovery server + nwmon Network monitor for skywire VPN and Visor. + pvm Public Visor monitor. + ssm Skysocks monitor. + vpnm VPN monitor. + + +``` + +#### svc sn + +``` + + ┌─┐┌─┐┌┬┐┬ ┬┌─┐ ┌┐┌┌─┐┌┬┐┌─┐ + └─┐├┤ │ │ │├─┘───││││ │ ││├┤ + └─┘└─┘ ┴ └─┘┴ ┘└┘└─┘─┴┘└─┘ + + + +Flags: + -m, --metrics string address to bind metrics API to + -i, --stdin read config from STDIN + --syslog string syslog server address. E.g. localhost:514 + --tag string logging tag (default "setup_node") + + +``` + +#### svc tpd + +``` + + ┌┬┐┬─┐┌─┐┌┐┌┌─┐┌─┐┌─┐┬─┐┌┬┐ ┌┬┐┬┌─┐┌─┐┌─┐┬ ┬┌─┐┬─┐┬ ┬ + │ ├┬┘├─┤│││└─┐├─┘│ │├┬┘ │───│││└─┐│ │ │└┐┌┘├┤ ├┬┘└┬┘ + ┴ ┴└─┴ ┴┘└┘└─┘┴ └─┘┴└─ ┴ ─┴┘┴└─┘└─┘└─┘ └┘ └─┘┴└─ ┴ +----- depends: redis, postgresql and initial DB setup ----- +sudo -iu postgres createdb tpd +keys-gen | tee tpd-config.json +PG_USER="postgres" PG_DATABASE="tpd" PG_PASSWORD="" transport-discovery --sk $(tail -n1 tpd-config.json) + + + +Flags: + -a, --addr string address to bind to (default ":9091") + --dmsg-disc string url of dmsg-discovery (default "http://dmsgd.skywire.skycoin.com") + --dmsgPort uint16 dmsg port value + (default 80) + -l, --loglvl string set log level one of: info, error, warn, debug, trace, panic (default "info") + -m, --metrics string address to bind metrics API to + --pg-host string host of postgres (default "localhost") + --pg-port string port of postgres (default "5432") + --redis string connections string for a redis store (default "redis://localhost:6379") + --redis-pool-size int redis connection pool size (default 10) + --sk cipher.SecKey dmsg secret key + (default 0000000000000000000000000000000000000000000000000000000000000000) + --syslog string syslog server address. E.g. localhost:514 + --tag string logging tag (default "transport_discovery") + --test-environment distinguished between prod and test environment + -t, --testing enable testing to start without redis + --whitelist-keys string list of whitelisted keys of network monitor used for deregistration + + +``` + +#### svc tps + +``` + + ┌┬┐┬─┐┌─┐┌┐┌┌─┐┌─┐┌─┐┬─┐┌┬┐ ┌─┐┌─┐┌┬┐┬ ┬┌─┐ + │ ├┬┘├─┤│││└─┐├─┘│ │├┬┘ │───└─┐├┤ │ │ │├─┘ + ┴ ┴└─┴ ┴┘└┘└─┘┴ └─┘┴└─ ┴ └─┘└─┘ ┴ └─┘┴ + + + +Flags: + -c, --config string path to config file + -l, --loglvl string set log level one of: info, error, warn, debug, trace, panic (default "info") + + +``` + +#### svc ar + +``` + + ┌─┐┌┬┐┌┬┐┬─┐┌─┐┌─┐┌─┐ ┬─┐┌─┐┌─┐┌─┐┬ ┬ ┬┌─┐┬─┐ + ├─┤ ││ ││├┬┘├┤ └─┐└─┐───├┬┘├┤ └─┐│ ││ └┐┌┘├┤ ├┬┘ + ┴ ┴─┴┘─┴┘┴└─└─┘└─┘└─┘ ┴└─└─┘└─┘└─┘┴─┘└┘ └─┘┴└─ + +depends: redis + +Note: the specified port must be accessible from the internet ip address or port forwarded for udp +skywire cli config gen-keys > ar-config.json +skywire svc ar --addr ":9093" --redis "redis://localhost:6379" --sk $(tail -n1 ar-config.json) + +Usage: + skywire svc ar + +Flags: + -a, --addr string address to bind to (default ":9093") + --dmsg-disc string url of dmsg-discovery (default "http://dmsgd.skywire.skycoin.com") + --dmsgPort uint16 dmsg port value + (default 80) + -l, --loglvl string set log level one of: info, error, warn, debug, trace, panic (default "info") + -m, --metrics string address to bind metrics API to + --redis string connections string for a redis store (default "redis://localhost:6379") + --redis-pool-size int redis connection pool size (default 10) + --sk cipher.SecKey dmsg secret key + (default 0000000000000000000000000000000000000000000000000000000000000000) + --syslog string syslog server address. E.g. localhost:514 + --tag string logging tag (default "address_resolver") + --test-environment distinguished between prod and test environment + -t, --testing enable testing to start without redis + --whitelist-keys string list of whitelisted keys of network monitor used for deregistration + + +``` + +#### svc rf + +``` + + ┬─┐┌─┐┬ ┬┌┬┐┌─┐ ┌─┐┬┌┐┌┌┬┐┌─┐┬─┐ + ├┬┘│ ││ │ │ ├┤───├┤ ││││ ││├┤ ├┬┘ + ┴└─└─┘└─┘ ┴ └─┘ └ ┴┘└┘─┴┘└─┘┴└─ +----- depends: postgres and initial db setup ----- +sudo -iu postgres createdb rf +skywire cli config gen-keys | tee rf-config.json +PG_USER="postgres" PG_DATABASE="rf" PG_PASSWORD="" route-finder --addr ":9092" --sk $(tail -n1 rf-config.json) + + + +Flags: + -a, --addr string address to bind to (default ":9092") + --dmsg-disc string url of dmsg-discovery (default "http://dmsgd.skywire.skycoin.com") + --dmsgPort uint16 dmsg port value + (default 80) + -l, --loglvl string set log level one of: info, error, warn, debug, trace, panic (default "info") + -m, --metrics string address to bind metrics API to + --pg-host string host of postgres (default "localhost") + --pg-port string port of postgres (default "5432") + --sk cipher.SecKey dmsg secret key + (default 0000000000000000000000000000000000000000000000000000000000000000) + --syslog string syslog server address. E.g. localhost:514 + --tag string logging tag (default "route_finder") + -t, --testing enable testing to start without redis + + +``` + +#### svc cb + +``` + + ┌─┐┌─┐┌┐┌┌─┐┬┌─┐ ┌┐ ┌─┐┌─┐┌┬┐┌─┐┌┬┐┬─┐┌─┐┌─┐┌─┐┌─┐┬─┐ + │ │ ││││├┤ ││ ┬───├┴┐│ ││ │ │ └─┐ │ ├┬┘├─┤├─┘├─┘├┤ ├┬┘ + └─┘└─┘┘└┘└ ┴└─┘ └─┘└─┘└─┘ ┴ └─┘ ┴ ┴└─┴ ┴┴ ┴ └─┘┴└─ + + + +Flags: + -a, --addr string address to bind to (default ":9082") + -c, --config string stun server list file location (default "./config.json") + --dmsg-disc string url of dmsg-discovery (default "http://dmsgd.skywire.skycoin.com") + --dmsgPort uint16 dmsg port value + (default 80) + -d, --domain string the domain of the endpoints (default "skywire.skycoin.com") + --sk cipher.SecKey dmsg secret key + (default 0000000000000000000000000000000000000000000000000000000000000000) + --tag string logging tag (default "address_resolver") + + +``` + +#### svc kg + +``` + + ┬┌─┌─┐┬ ┬┌─┐ ┌─┐┌─┐┌┐┌ + ├┴┐├┤ └┬┘└─┐───│ ┬├┤ │││ + ┴ ┴└─┘ ┴ └─┘ └─┘└─┘┘└┘ + + + + +``` + +#### svc lc + +``` + + ┬ ┬┬ ┬┌─┐┌┐┌┌─┐┌─┐┌─┐ ┌─┐┬ ┬┌─┐┌─┐┬┌─┌─┐┬─┐ + │ │└┐┌┘├┤ │││├┤ └─┐└─┐───│ ├─┤├┤ │ ├┴┐├┤ ├┬┘ + ┴─┘┴ └┘ └─┘┘└┘└─┘└─┘└─┘ └─┘┴ ┴└─┘└─┘┴ ┴└─┘┴└─ + + + +Flags: + -a, --addr string address to bind to. (default ":9081") + -c, --config string config file location. (default "liveness-checker.json") + -l, --loglvl string set log level one of: info, error, warn, debug, trace, panic (default "info") + --redis string connections string for a redis store (default "redis://localhost:6379") + --syslog string syslog server address. E.g. localhost:514 + --tag string logging tag (default "liveness_checker") + -t, --testing enable testing to start without redis + + +``` + +#### svc nv + +``` + + ┌┐┌┌─┐┌┬┐┌─┐ ┬ ┬┬┌─┐┬ ┬┌─┐┬ ┬┌─┐┌─┐┬─┐ + ││││ │ ││├┤───└┐┌┘│└─┐│ │├─┤│ │┌─┘├┤ ├┬┘ + ┘└┘└─┘─┴┘└─┘ └┘ ┴└─┘└─┘┴ ┴┴─┘┴└─┘└─┘┴└─ + + + +Flags: + -a, --addr string address to bind to (default ":9081") + -l, --log enable request logging (default true) + -m, --metrics string address to bind metrics API to + --syslog string syslog server address. E.g. localhost:514 + --tag string logging tag (default "node-visualizer") + -t, --testing enable testing to start without redis + + +``` + +#### svc se + +``` + + ┌─┐┬ ┬ ┌─┐┌┐┌┬ ┬ + └─┐│││───├┤ │││└┐┌┘ + └─┘└┴┘ └─┘┘└┘ └┘ + +Available Commands: + visor Generate config for skywire-visor + dmsg Generate config for dmsg-server + setup Generate config for setup node + +Flags: + -d, --docker Environment with dockerized skywire-services + -l, --local Environment with skywire-services on localhost + -n, --network string Docker network to use (default "SKYNET") + -p, --public Environment with public skywire-services + + +``` + +##### svc se visor + +``` +Generate config for skywire-visor + + + + +``` + +##### svc se dmsg + +``` +Generate config for dmsg-server + + + + +``` + +##### svc se setup + +``` +Generate config for setup node + + + + +``` + +#### svc sd + +``` + + ┌─┐┌─┐┬─┐┬ ┬┬┌─┐┌─┐ ┌┬┐┬┌─┐┌─┐┌─┐┬ ┬┌─┐┬─┐┬ ┬ + └─┐├┤ ├┬┘└┐┌┘││ ├┤───│││└─┐│ │ │└┐┌┘├┤ ├┬┘└┬┘ + └─┘└─┘┴└─ └┘ ┴└─┘└─┘ ─┴┘┴└─┘└─┘└─┘ └┘ └─┘┴└─ ┴ +----- depends: redis, postgresql and initial DB setup ----- +sudo -iu postgres createdb sd +keys-gen | tee sd-config.json +PG_USER="postgres" PG_DATABASE="sd" PG_PASSWORD="" service-discovery --sk $(tail -n1 sd-config.json) + + + +Flags: + -a, --addr string address to bind to (default ":9098") + -g, --api-key string geo API key + -d, --dmsg-disc string url of dmsg-discovery (default "http://dmsgd.skywire.skycoin.com") + --dmsgPort uint16 dmsg port value (default 80) + -m, --metrics string address to bind metrics API to + -o, --pg-host string host of postgres (default "localhost") + -p, --pg-port string port of postgres (default "5432") + -r, --redis string connections string for a redis store (default "redis://localhost:6379") + -s, --sk cipher.SecKey dmsg secret key + (default 0000000000000000000000000000000000000000000000000000000000000000) + -t, --test run in test mode and disable auth + -n, --test-environment distinguished between prod and test environment + -w, --whitelist-keys string list of whitelisted keys of network monitor used for deregistration + + +``` + +#### svc nwmon + +``` + + ┌┐┌┌─┐┌┬┐┬ ┬┌─┐┬─┐┬┌─ ┌┬┐┌─┐┌┐┌┬┌┬┐┌─┐┬─┐ + │││├┤ │ ││││ │├┬┘├┴┐───││││ │││││ │ │ │├┬┘ + ┘└┘└─┘ ┴ └┴┘└─┘┴└─┴ ┴ ┴ ┴└─┘┘└┘┴ ┴ └─┘┴└─ + + + +Flags: + -a, --addr string address to bind to. (default ":9080") + -v, --ar-url string url to address resolver. + -b, --batchsize int Batch size of deregistration (default 30) + -c, --config string config file location. (default "network-monitor.json") + -l, --loglvl string set log level one of: info, error, warn, debug, trace, panic (default "info") + -m, --metrics string address to bind metrics API to + --redis string connections string for a redis store (default "redis://localhost:6379") + --redis-pool-size int redis connection pool size (default 10) + -n, --sd-url string url to service discovery. + --sleep-deregistration duration Sleep time for derigstration process in minutes (default 10ns) + --syslog string syslog server address. E.g. localhost:514 + --tag string logging tag (default "network_monitor") + -t, --testing enable testing to start without redis + -u, --ut-url string url to uptime tracker visor data. + + +``` + +#### svc pvm + +``` + + ┌─┐┬ ┬┌┐ ┬ ┬┌─┐ ┬ ┬┬┌─┐┌─┐┬─┐ ┌┬┐┌─┐┌┐┌┬┌┬┐┌─┐┬─┐ + ├─┘│ │├┴┐│ ││───└┐┌┘│└─┐│ │├┬┘───││││ │││││ │ │ │├┬┘ + ┴ └─┘└─┘┴─┘┴└─┘ └┘ ┴└─┘└─┘┴└─ ┴ ┴└─┘┘└┘┴ ┴ └─┘┴└─ + + + +Flags: + -a, --addr string address to bind to. (default ":9082") + -c, --config string config file location. (default "public-visor-monitor.json") + -l, --loglvl string set log level one of: info, error, warn, debug, trace, panic (default "info") + -s, --sleep-deregistration duration Sleep time for derigstration process in minutes (default 10ns) + --tag string logging tag (default "public_visor_monitor") + + +``` + +#### svc ssm + +``` + + ┌─┐┬┌─┬ ┬┌─┐┌─┐┌─┐┬┌─┌─┐ ┌┬┐┌─┐┌┐┌┬┌┬┐┌─┐┬─┐ + └─┐├┴┐└┬┘└─┐│ ││ ├┴┐└─┐───││││ │││││ │ │ │├┬┘ + └─┘┴ ┴ ┴ └─┘└─┘└─┘┴ ┴└─┘ ┴ ┴└─┘┘└┘┴ ┴ └─┘┴└─ + + + +Flags: + -a, --addr string address to bind to. (default ":9081") + -c, --config string config file location. (default "skysocks-monitor.json") + -s, --sleep-deregistration duration Sleep time for derigstration process in minutes (default 10ns) + --tag string logging tag (default "skysocks_monitor") + + +``` + +#### svc vpnm + +``` + + ┬ ┬┌─┐┌┐┌ ┌┬┐┌─┐┌┐┌┬┌┬┐┌─┐┬─┐ + └┐┌┘├─┘│││───││││ │││││ │ │ │├┬┘ + └┘ ┴ ┘└┘ ┴ ┴└─┘┘└┘┴ ┴ └─┘┴└─ + + + +Flags: + -a, --addr string address to bind to. (default ":9081") + -c, --config string config file location. (default "vpn-monitor.json") + -s, --sleep-deregistration duration Sleep time for derigstration process in minutes (default 10ns) + --tag string logging tag (default "vpn_monitor") + + +``` + +### dmsg + +``` + + ┌┬┐┌┬┐┌─┐┌─┐ + │││││└─┐│ ┬ + ─┴┘┴ ┴└─┘└─┘ + +Available Commands: + pty Dmsg pseudoterminal (pty) + disc DMSG Discovery Server + server DMSG Server + http DMSG http file server + curl DMSG curl utility + web DMSG resolving proxy & browser client + socks DMSG socks5 proxy server & client + mon DMSG monitor of DMSG discovery entries. + + +``` + +#### dmsg pty + +``` + + ┌─┐┌┬┐┬ ┬ + ├─┘ │ └┬┘ + ┴ ┴ ┴ + +Available Commands: + cli DMSG pseudoterminal command line interface + host DMSG host for pseudoterminal command line interface + ui DMSG pseudoterminal GUI + + +``` + +##### dmsg pty cli + +``` + + ┌┬┐┌┬┐┌─┐┌─┐┌─┐┌┬┐┬ ┬ ┌─┐┬ ┬ + │││││└─┐│ ┬├─┘ │ └┬┘───│ │ │ + ─┴┘┴ ┴└─┘└─┘┴ ┴ ┴ └─┘┴─┘┴ +DMSG pseudoterminal command line interface + +Available Commands: + whitelist lists all whitelisted public keys + whitelist-add adds public key(s) to the whitelist + whitelist-remove removes public key(s) from the whitelist + +Flags: + --addr dmsg.Addr remote dmsg address of format 'pk:port' + If unspecified, the pty will start locally + (default 000000000000000000000000000000000000000000000000000000000000000000:~) + -a, --args strings command arguments + -r, --cliaddr string address to use for dialing to dmsgpty-host (default "/tmp/dmsgpty.sock") + -n, --clinet string network to use for dialing to dmsgpty-host (default "unix") + -c, --cmd string name of command to run + (default "/bin/bash") + -p, --confpath string config path (default "config.json") + + +``` + +###### dmsg pty cli whitelist + +``` +lists all whitelisted public keys + + + + +``` + +###### dmsg pty cli whitelist-add + +``` +adds public key(s) to the whitelist + + + + +``` + +###### dmsg pty cli whitelist-remove + +``` +removes public key(s) from the whitelist + + + + +``` + +##### dmsg pty host + +``` + + ┌┬┐┌┬┐┌─┐┌─┐┌─┐┌┬┐┬ ┬ ┬ ┬┌─┐┌─┐┌┬┐ + │││││└─┐│ ┬├─┘ │ └┬┘───├─┤│ │└─┐ │ + ─┴┘┴ ┴└─┘└─┘┴ ┴ ┴ ┴ ┴└─┘└─┘ ┴ +DMSG host for pseudoterminal command line interface + +Available Commands: + confgen generates config file + +Flags: + --cliaddr string address used for listening for cli connections (default "/tmp/dmsgpty.sock") + --clinet string network used for listening for cli connections (default "unix") + -c, --confpath string config path (default "./config.json") + --confstdin config will be read from stdin if set + --dmsgdisc string dmsg discovery address (default "http://dmsgd.skywire.skycoin.com") + --dmsgport uint16 dmsg port for listening for remote hosts (default 22) + --dmsgsessions int minimum number of dmsg sessions to ensure (default 1) + --envprefix string env prefix (default "DMSGPTY") + --wl cipher.PubKeys whitelist of the dmsgpty-host (default public keys: + ) + + +``` + +###### dmsg pty host confgen + +``` +generates config file + + + +Flags: + --unsafe will unsafely write config if set + + +``` + +##### dmsg pty ui + +``` + + ┌┬┐┌┬┐┌─┐┌─┐┌─┐┌┬┐┬ ┬ ┬ ┬┬ + │││││└─┐│ ┬├─┘ │ └┬┘───│ ││ + ─┴┘┴ ┴└─┘└─┘┴ ┴ ┴ └─┘┴ + DMSG pseudoterminal GUI + + + +Flags: + --addr string network address to serve UI on (default ":8080") + --arg stringArray command arguments to include when initiating pty + --cmd string command to run when initiating pty (default "/bin/bash") + --haddr string dmsgpty host network address (default "/tmp/dmsgpty.sock") + --hnet string dmsgpty host network name (default "unix") + + +``` + +#### dmsg disc + +``` + + ┌┬┐┌┬┐┌─┐┌─┐ ┌┬┐┬┌─┐┌─┐┌─┐┬ ┬┌─┐┬─┐┬ ┬ + │││││└─┐│ ┬───│││└─┐│ │ │└┐┌┘├┤ ├┬┘└┬┘ + ─┴┘┴ ┴└─┘└─┘ ─┴┘┴└─┘└─┘└─┘ └┘ └─┘┴└─ ┴ +DMSG Discovery Server +----- depends: redis ----- +skywire cli config gen-keys > dmsgd-config.json +skywire dmsg disc --sk $(tail -n1 dmsgd-config.json) + + + +Flags: + -a, --addr string address to bind to (default ":9090") + --auth string auth passphrase as simple auth for official dmsg servers registration + --dmsgPort uint16 dmsg port value (default 80) + --enable-load-testing enable load testing + --entry-timeout duration discovery entry timeout (default 3m0s) + -m, --metrics string address to serve metrics API from + --official-servers string list of official dmsg servers keys separated by comma + --redis string connections string for a redis store (default "redis://localhost:6379") + --sk cipher.SecKey dmsg secret key + (default 0000000000000000000000000000000000000000000000000000000000000000) + --syslog string address in which to dial to syslog server + --syslog-lvl string minimum log level to report (default "debug") + --syslog-net string network in which to dial to syslog server (default "udp") + --tag string tag used for logging and metrics (default "dmsg_disc") + --test-environment distinguished between prod and test environment + -t, --test-mode in testing mode + --whitelist-keys string list of whitelisted keys of network monitor used for deregistration + + +``` + +#### dmsg server + +``` + + ┌┬┐┌┬┐┌─┐┌─┐ ┌─┐┌─┐┬─┐┬ ┬┌─┐┬─┐ + ││││││└─┐│ ┬ ─ └─┐├┤ ├┬┘└┐┌┘├┤ ├┬┘ + ─┴┘┴ ┴└─┘└─┘ └─┘└─┘┴└─ └┘ └─┘┴└─ +DMSG Server +skywire dmsg server config gen -o dmsg-config.json +skywire dmsg server start dmsg-config.json + +Available Commands: + config Generate a dmsg-server config + start Start Dmsg Server + + +``` + +##### dmsg server config + +``` +Generate a dmsg-server config + +Available Commands: + gen Generate a config file + + +``` + +###### dmsg server config gen + +``` +Generate a config file + + + +Flags: + -o, --output string config output path/name + -t, --testenv use test deployment + + +``` + +##### dmsg server start + +``` +Start Dmsg Server + + + +Flags: + --auth string auth passphrase as simple auth for official dmsg servers registration + -c, --config string location of config file (STDIN to read from standard input) (default "config.json") + --limit-ip int set limitation of IPs want connect to specific dmsg-server, default value is 15 (default 15) + -m, --metrics string address to serve metrics API from + --stdin whether to read config via stdin + --syslog string address in which to dial to syslog server + --syslog-lvl string minimum log level to report (default "debug") + --syslog-net string network in which to dial to syslog server (default "udp") + --tag string tag used for logging and metrics (default "dmsg_srv") + + +``` + +#### dmsg http + +``` + + ┌┬┐┌┬┐┌─┐┌─┐┬ ┬┌┬┐┌┬┐┌─┐ + │││││└─┐│ ┬├─┤ │ │ ├─┘ + ─┴┘┴ ┴└─┘└─┘┴ ┴ ┴ ┴ ┴ +DMSG http file server + + + +Flags: + -d, --dir string local dir to serve via dmsghttp (default ".") + -D, --dmsg-disc string dmsg discovery url default: + http://dmsgd.skywire.skycoin.com + -p, --port uint dmsg port to serve from (default 80) + -s, --sk cipher.SecKey a random key is generated if unspecified + + (default 0000000000000000000000000000000000000000000000000000000000000000) + -w, --wl string whitelist keys, comma separated + + +``` + +#### dmsg curl + +``` + + ┌┬┐┌┬┐┌─┐┌─┐┌─┐┬ ┬┬─┐┬ + │││││└─┐│ ┬│ │ │├┬┘│ + ─┴┘┴ ┴└─┘└─┘└─┘└─┘┴└─┴─┘ +DMSG curl utility + + + +Flags: + -a, --agent AGENT identify as AGENT (default "dmsgcurl/unknown") + -d, --data string dmsghttp POST data + -c, --dmsg-disc string dmsg discovery url default: + http://dmsgd.skywire.skycoin.com + -l, --loglvl string [ debug | warn | error | fatal | panic | trace | info ] (default "fatal") + -o, --out string output filepath + -r, --replace replace exist file with new downloaded + -e, --sess int number of dmsg servers to connect to (default 1) + -s, --sk cipher.SecKey a random key is generated if unspecified + + (default 0000000000000000000000000000000000000000000000000000000000000000) + -t, --try int download attempts (0 unlimits) (default 1) + -w, --wait int time to wait between fetches + + +``` + +#### dmsg web + +``` + + ┌┬┐┌┬┐┌─┐┌─┐┬ ┬┌─┐┌┐ + │││││└─┐│ ┬│││├┤ ├┴┐ + ─┴┘┴ ┴└─┘└─┘└┴┘└─┘└─┘ +DMSG resolving proxy & browser client - access websites over dmsg + +Available Commands: + gen-keys generate public / secret keypair + +Flags: + -d, --dmsg-disc string dmsg discovery url default: + http://dmsgd.skywire.skycoin.com + -f, --filter string domain suffix to filter (default ".dmsg") + -l, --loglvl string [ debug | warn | error | fatal | panic | trace | info ] + -p, --port string port to serve the web application (default "8080") + -r, --proxy string configure additional socks5 proxy for dmsgweb (i.e. 127.0.0.1:1080) + -t, --resolve string resolve the specified dmsg address:port on the local port & disable proxy + -e, --sess int number of dmsg servers to connect to (default 1) + -s, --sk cipher.SecKey a random key is generated if unspecified + + (default 0000000000000000000000000000000000000000000000000000000000000000) + -q, --socks string port to serve the socks5 proxy (default "4445") + + +``` + +##### dmsg web gen-keys + +``` +generate public / secret keypair + + + + +``` + +#### dmsg socks + +``` + + ┌┬┐┌┬┐┌─┐┌─┐ ┌─┐┌─┐┌─┐┬┌─┌─┐ + │││││└─┐│ ┬───└─┐│ ││ ├┴┐└─┐ + ─┴┘┴ ┴└─┘└─┘ └─┘└─┘└─┘┴ ┴└─┘ +DMSG socks5 proxy server & client + +Available Commands: + server dmsg socks5 proxy server + client socks5 proxy client for dmsg socks5 proxy server + + +``` + +##### dmsg socks server + +``` +dmsg socks5 proxy server + + + +Flags: + -D, --dmsg-disc string dmsg discovery url (default "http://dmsgd.skywire.skycoin.com") + -q, --dport uint16 dmsg port to serve socks5 (default 1081) + -s, --sk cipher.SecKey a random key is generated if unspecified + + (default 0000000000000000000000000000000000000000000000000000000000000000) + -w, --wl string whitelist keys, comma separated + + +``` + +##### dmsg socks client + +``` +socks5 proxy client for dmsg socks5 proxy server + + + +Flags: + -D, --dmsg-disc string dmsg discovery url (default "http://dmsgd.skywire.skycoin.com") + -q, --dport uint16 dmsg port to connect to socks5 server (default 1081) + -k, --pk string dmsg socks5 proxy server public key to connect to + -p, --port int TCP port to serve SOCKS5 proxy locally (default 1081) + -s, --sk cipher.SecKey a random key is generated if unspecified + + (default 0000000000000000000000000000000000000000000000000000000000000000) + + +``` + +#### dmsg mon + +``` + + ┌┬┐┌┬┐┌─┐┌─┐ ┌┬┐┌─┐┌┐┌┬┌┬┐┌─┐┬─┐ + │││││└─┐│ ┬───││││ │││││ │ │ │├┬┘ + ─┴┘┴ ┴└─┘└─┘ ┴ ┴└─┘┘└┘┴ ┴ └─┘┴└─ + + + +Flags: + -a, --addr string address to bind to. (default ":9080") + -b, --batchsize int Batch size of deregistration (default 20) + -c, --config string config file location. (default "dmsg-monitor.json") + -d, --dmsg-url string url to dmsg data. + -l, --loglvl string set log level one of: info, error, warn, debug, trace, panic (default "info") + -s, --sleep-deregistration duration Sleep time for derigstration process in minutes (default 60ns) + --syslog string syslog server address. E.g. localhost:514 + --tag string logging tag (default "dmsg_monitor") + -u, --ut-url string url to uptime tracker visor data. + + +``` + +### app + +``` + + ┌─┐┌─┐┌─┐┌─┐ + ├─┤├─┘├─┘└─┐ + ┴ ┴┴ ┴ └─┘ + +Available Commands: + vpn-server skywire vpn server application + vpn-client skywire vpn client application + skysocks-client skywire socks5 proxy client application + skysocks skywire socks5 proxy server application + skychat skywire chat application + + +``` + +#### app vpn-server + +``` + + ┬ ┬┌─┐┌┐┌ ┌─┐┌─┐┬─┐┬ ┬┌─┐┬─┐ + └┐┌┘├─┘│││───└─┐├┤ ├┬┘└┐┌┘├┤ ├┬┘ + └┘ ┴ ┘└┘ └─┘└─┘┴└─ └┘ └─┘┴└─ + + + +Flags: + --netifc string Default network interface for multiple available interfaces + --passcode string passcode to authenticate connecting users + --pk string local pubkey + --secure Forbid connections from clients to server local network (default true) + --sk string local seckey + + +``` + +#### app vpn-client + +``` + + ┬ ┬┌─┐┌┐┌ ┌─┐┬ ┬┌─┐┌┐┌┌┬┐ + └┐┌┘├─┘│││───│ │ │├┤ │││ │ + └┘ ┴ ┘└┘ └─┘┴─┘┴└─┘┘└┘ ┴ + + + +Flags: + --dns string address of DNS want set to tun + --killswitch If set, the Internet won't be restored during reconnection attempts + --passcode string passcode to authenticate connection + --pk string local pubkey + --sk string local seckey + --srv string PubKey of the server to connect to + + +``` + +#### app skysocks-client + +``` + + ┌─┐┬┌─┬ ┬┌─┐┌─┐┌─┐┬┌─┌─┐ ┌─┐┬ ┬┌─┐┌┐┌┌┬┐ + └─┐├┴┐└┬┘└─┐│ ││ ├┴┐└─┐───│ │ │├┤ │││ │ + └─┘┴ ┴ ┴ └─┘└─┘└─┘┴ ┴└─┘ └─┘┴─┘┴└─┘┘└┘ ┴ + + + +Flags: + --addr string Client address to listen on (default ":1080") + --http string http proxy mode + --srv string PubKey of the server to connect to + + +``` + +#### app skysocks + +``` + + ┌─┐┬┌─┬ ┬┌─┐┌─┐┌─┐┬┌─┌─┐ + └─┐├┴┐└┬┘└─┐│ ││ ├┴┐└─┐ + └─┘┴ ┴ ┴ └─┘└─┘└─┘┴ ┴└─┘ + + + +Flags: + --passcode string passcode to authenticate connecting users + + +``` + +#### app skychat + +``` + + ┌─┐┬┌─┬ ┬┌─┐┬ ┬┌─┐┌┬┐ + └─┐├┴┐└┬┘│ ├─┤├─┤ │ + └─┘┴ ┴ ┴ └─┘┴ ┴┴ ┴ ┴ + + + +Flags: + --addr string address to bind, put an * before the port if you want to be able to access outside localhost (default ":8001") + + +``` + +### tree + +``` +subcommand tree + + + + +``` + +### doc + +``` +generate markdown docs + + UNHIDEFLAGS=1 go run cmd/skywire/skywire.go doc + + UNHIDEFLAGS=1 go run cmd/skywire/skywire.go doc > cmd/skywire/README1.md + + generate toc: + + cat cmd/skywire/README1.md | gh-md-toc + + + + +``` diff --git a/cmd/skywire/skywire.go b/cmd/skywire/skywire.go index ac05f801e..08466e37d 100644 --- a/cmd/skywire/skywire.go +++ b/cmd/skywire/skywire.go @@ -1,38 +1,160 @@ -// /* cmd/skywire-visor/skywire-visor.go +// cmd/skywire/skywire.go /* -skywire visor +skywire */ package main import ( "fmt" + "log" + "os" + "path/filepath" + "strings" + "github.com/bitfield/script" cc "github.com/ivanpirog/coloredcobra" + "github.com/pterm/pterm" + "github.com/pterm/pterm/putils" + dmsgdisc "github.com/skycoin/dmsg/cmd/dmsg-discovery/commands" + dmsgserver "github.com/skycoin/dmsg/cmd/dmsg-server/commands" + dmsgsocks "github.com/skycoin/dmsg/cmd/dmsg-socks5/commands" + dmsgcurl "github.com/skycoin/dmsg/cmd/dmsgcurl/commands" + dmsghttp "github.com/skycoin/dmsg/cmd/dmsghttp/commands" + dmsgptycli "github.com/skycoin/dmsg/cmd/dmsgpty-cli/commands" + dmsgptyhost "github.com/skycoin/dmsg/cmd/dmsgpty-host/commands" + dmsgptyui "github.com/skycoin/dmsg/cmd/dmsgpty-ui/commands" + dmsgweb "github.com/skycoin/dmsg/cmd/dmsgweb/commands" + sd "github.com/skycoin/skycoin-service-discovery/cmd/service-discovery/commands" "github.com/spf13/cobra" + ar "github.com/skycoin/skywire-services/cmd/address-resolver/commands" + confbs "github.com/skycoin/skywire-services/cmd/config-bootstrapper/commands" + dmsgmon "github.com/skycoin/skywire-services/cmd/dmsg-monitor/commands" + kg "github.com/skycoin/skywire-services/cmd/keys-gen/commands" + lc "github.com/skycoin/skywire-services/cmd/liveness-checker/commands" + nwmon "github.com/skycoin/skywire-services/cmd/network-monitor/commands" + nv "github.com/skycoin/skywire-services/cmd/node-visualizer/commands" + pvmon "github.com/skycoin/skywire-services/cmd/public-visor-monitor/commands" + rf "github.com/skycoin/skywire-services/cmd/route-finder/commands" + ssmon "github.com/skycoin/skywire-services/cmd/skysocks-monitor/commands" + se "github.com/skycoin/skywire-services/cmd/sw-env/commands" + tpd "github.com/skycoin/skywire-services/cmd/transport-discovery/commands" + tps "github.com/skycoin/skywire-services/cmd/transport-setup/commands" + vpnmon "github.com/skycoin/skywire-services/cmd/vpn-monitor/commands" "github.com/skycoin/skywire-utilities/pkg/buildinfo" - setupnode "github.com/skycoin/skywire/cmd/setup-node/commands" - skywirecli "github.com/skycoin/skywire/cmd/skywire-cli/commands" + sc "github.com/skycoin/skywire/cmd/apps/skychat/commands" + ssc "github.com/skycoin/skywire/cmd/apps/skysocks-client/commands" + ss "github.com/skycoin/skywire/cmd/apps/skysocks/commands" + vpnc "github.com/skycoin/skywire/cmd/apps/vpn-client/commands" + vpns "github.com/skycoin/skywire/cmd/apps/vpn-server/commands" + sn "github.com/skycoin/skywire/cmd/setup-node/commands" + scli "github.com/skycoin/skywire/cmd/skywire-cli/commands" "github.com/skycoin/skywire/pkg/visor" ) func init() { - rootCmd.AddCommand( + dmsgptyCmd.AddCommand( + dmsgptycli.RootCmd, + dmsgptyhost.RootCmd, + dmsgptyui.RootCmd, + ) + dmsgCmd.AddCommand( + dmsgptyCmd, + dmsgdisc.RootCmd, + dmsgserver.RootCmd, + dmsghttp.RootCmd, + dmsgcurl.RootCmd, + dmsgweb.RootCmd, + dmsgsocks.RootCmd, + dmsgmon.RootCmd, + ) + svcCmd.AddCommand( + sn.RootCmd, + tpd.RootCmd, + tps.RootCmd, + ar.RootCmd, + rf.RootCmd, + confbs.RootCmd, + kg.RootCmd, + lc.RootCmd, + nv.RootCmd, + se.RootCmd, + sd.RootCmd, + nwmon.RootCmd, + pvmon.RootCmd, + ssmon.RootCmd, + vpnmon.RootCmd, + ) + appsCmd.AddCommand( + vpns.RootCmd, + vpnc.RootCmd, + ssc.RootCmd, + ss.RootCmd, + sc.RootCmd, + ) + RootCmd.AddCommand( visor.RootCmd, - skywirecli.RootCmd, - setupnode.RootCmd, + scli.RootCmd, + svcCmd, + dmsgCmd, + appsCmd, + treeCmd, + docCmd, ) + visor.RootCmd.Long = ` + ┌─┐┬┌─┬ ┬┬ ┬┬┬─┐┌─┐ ┬ ┬┬┌─┐┌─┐┬─┐ + └─┐├┴┐└┬┘││││├┬┘├┤───└┐┌┘│└─┐│ │├┬┘ + └─┘┴ ┴ ┴ └┴┘┴┴└─└─┘ └┘ ┴└─┘└─┘┴└─` + dmsgcurl.RootCmd.Use = "curl" + dmsgweb.RootCmd.Use = "web" + dmsgptycli.RootCmd.Use = "cli" + dmsgptyhost.RootCmd.Use = "host" + dmsgptyui.RootCmd.Use = "ui" + dmsgdisc.RootCmd.Use = "disc" + dmsgserver.RootCmd.Use = "server" + dmsghttp.RootCmd.Use = "http" + dmsgcurl.RootCmd.Use = "curl" + dmsgweb.RootCmd.Use = "web" + dmsgsocks.RootCmd.Use = "socks" + dmsgmon.RootCmd.Use = "mon" + tpd.RootCmd.Use = "tpd" + tps.RootCmd.Use = "tps" + ar.RootCmd.Use = "ar" + rf.RootCmd.Use = "rf" + confbs.RootCmd.Use = "cb" + kg.RootCmd.Use = "kg" + lc.RootCmd.Use = "lc" + nv.RootCmd.Use = "nv" + vpnmon.RootCmd.Use = "vpnm" + pvmon.RootCmd.Use = "pvm" + ssmon.RootCmd.Use = "ssm" + nwmon.RootCmd.Use = "nwmon" + se.RootCmd.Use = "se" + sd.RootCmd.Use = "sd" + sn.RootCmd.Use = "sn" + scli.RootCmd.Use = "cli" + visor.RootCmd.Use = "visor" + vpns.RootCmd.Use = "vpn-server" + vpnc.RootCmd.Use = "vpn-client" + ssc.RootCmd.Use = "skysocks-client" + ss.RootCmd.Use = "skysocks" + sc.RootCmd.Use = "skychat" + var helpflag bool - rootCmd.SetUsageTemplate(help) - rootCmd.PersistentFlags().BoolVarP(&helpflag, "help", "h", false, "help for "+rootCmd.Use) - rootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) - rootCmd.PersistentFlags().MarkHidden("help") //nolint - rootCmd.CompletionOptions.DisableDefaultCmd = true + RootCmd.SetUsageTemplate(help) + RootCmd.PersistentFlags().BoolVarP(&helpflag, "help", "h", false, "help for "+RootCmd.Use) + RootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) + RootCmd.PersistentFlags().MarkHidden("help") //nolint + RootCmd.CompletionOptions.DisableDefaultCmd = true + RootCmd.SetUsageTemplate(help) } -var rootCmd = &cobra.Command{ - Use: "skywire", +// RootCmd contains literally every 'command' from four repos here +var RootCmd = &cobra.Command{ + Use: func() string { + return strings.Split(filepath.Base(strings.ReplaceAll(strings.ReplaceAll(fmt.Sprintf("%v", os.Args), "[", ""), "]", "")), " ")[0] + }(), Long: ` ┌─┐┬┌─┬ ┬┬ ┬┬┬─┐┌─┐ └─┐├┴┐└┬┘││││├┬┘├┤ @@ -44,9 +166,222 @@ var rootCmd = &cobra.Command{ Version: buildinfo.Version(), } +// RootCmd contains all subcommands +var svcCmd = &cobra.Command{ + Use: "svc", + Short: "Skywire services", + Long: ` + ┌─┐┬┌─┬ ┬┬ ┬┬┬─┐┌─┐ ┌─┐┌─┐┬─┐┬ ┬┬┌─┐┌─┐┌─┐ + └─┐├┴┐└┬┘││││├┬┘├┤───└─┐├┤ ├┬┘└┐┌┘││ ├┤ └─┐ + └─┘┴ ┴ ┴ └┴┘┴┴└─└─┘ └─┘└─┘┴└─ └┘ ┴└─┘└─┘└─┘`, + SilenceErrors: true, + SilenceUsage: true, + DisableSuggestions: true, + DisableFlagsInUseLine: true, + Version: buildinfo.Version(), +} + +// RootCmd contains all binaries which may be separately compiled as subcommands +var dmsgCmd = &cobra.Command{ + Use: "dmsg", + Short: "Dmsg services & utilities", + Long: ` + ┌┬┐┌┬┐┌─┐┌─┐ + │││││└─┐│ ┬ + ─┴┘┴ ┴└─┘└─┘ `, + SilenceErrors: true, + SilenceUsage: true, + DisableSuggestions: true, + DisableFlagsInUseLine: true, +} + +var dmsgptyCmd = &cobra.Command{ + Use: "pty", + Short: "Dmsg pseudoterminal (pty)", + Long: ` + ┌─┐┌┬┐┬ ┬ + ├─┘ │ └┬┘ + ┴ ┴ ┴ `, + SilenceErrors: true, + SilenceUsage: true, + DisableSuggestions: true, + DisableFlagsInUseLine: true, +} +var appsCmd = &cobra.Command{ + Use: "app", + Short: "skywire native applications", + Long: ` + ┌─┐┌─┐┌─┐┌─┐ + ├─┤├─┘├─┘└─┐ + ┴ ┴┴ ┴ └─┘`, + SilenceErrors: true, + SilenceUsage: true, + DisableSuggestions: true, + DisableFlagsInUseLine: true, +} + +var treeCmd = &cobra.Command{ + Use: "tree", + Short: "subcommand tree", + Long: `subcommand tree`, + SilenceErrors: true, + SilenceUsage: true, + DisableSuggestions: true, + DisableFlagsInUseLine: true, + Run: func(cmd *cobra.Command, args []string) { + // You can use a LeveledList here, for easy generation. + leveledList := pterm.LeveledList{} + leveledList = append(leveledList, pterm.LeveledListItem{Level: 0, Text: RootCmd.Use}) + for _, j := range RootCmd.Commands() { + use := strings.Split(j.Use, " ") + leveledList = append(leveledList, pterm.LeveledListItem{Level: 1, Text: use[0]}) + for _, k := range j.Commands() { + use := strings.Split(k.Use, " ") + leveledList = append(leveledList, pterm.LeveledListItem{Level: 2, Text: use[0]}) + for _, l := range k.Commands() { + use := strings.Split(l.Use, " ") + leveledList = append(leveledList, pterm.LeveledListItem{Level: 3, Text: use[0]}) + for _, m := range l.Commands() { + use := strings.Split(m.Use, " ") + leveledList = append(leveledList, pterm.LeveledListItem{Level: 4, Text: use[0]}) + for _, n := range m.Commands() { + use := strings.Split(n.Use, " ") + leveledList = append(leveledList, pterm.LeveledListItem{Level: 5, Text: use[0]}) + for _, o := range n.Commands() { + use := strings.Split(o.Use, " ") + leveledList = append(leveledList, pterm.LeveledListItem{Level: 6, Text: use[0]}) + for _, p := range o.Commands() { + use := strings.Split(p.Use, " ") + leveledList = append(leveledList, pterm.LeveledListItem{Level: 7, Text: use[0]}) + } + } + } + } + } + } + } + // Generate tree from LeveledList. + r := putils.TreeFromLeveledList(leveledList) + + // Render TreePrinter + err := pterm.DefaultTree.WithRoot(r).Render() + if err != nil { + log.Fatal("render subcommand tree: ", err) + } + }, +} + +// for toc generation use: https://github.com/ekalinin/github-markdown-toc.go +var docCmd = &cobra.Command{ + Use: "doc", + Short: "generate markdown docs", + Long: `generate markdown docs + + UNHIDEFLAGS=1 go run cmd/skywire/skywire.go doc + + UNHIDEFLAGS=1 go run cmd/skywire/skywire.go doc > cmd/skywire/README1.md + + generate toc: + + cat cmd/skywire/README1.md | gh-md-toc`, + SilenceErrors: true, + SilenceUsage: true, + DisableSuggestions: true, + DisableFlagsInUseLine: true, + Run: func(cmd *cobra.Command, args []string) { + fmt.Printf("\n# %s\n", "skywire documentation") + fmt.Printf("\n## %s\n", "subcommand tree") + fmt.Printf("\n%s\n", "A tree representation of the skywire subcommands") + fmt.Printf("\n```\n") + _, err := script.Exec(os.Args[0] + " tree").Stdout() //nolint + if err != nil { + fmt.Println(err.Error()) + } + fmt.Printf("\n```\n") + + var use string + for _, j := range RootCmd.Commands() { + use = strings.Split(j.Use, " ")[0] + fmt.Printf("\n### %s\n", use) + fmt.Printf("\n```\n") + j.Help() //nolint + fmt.Printf("\n```\n") + if j.Name() == "cli" { + fmt.Printf("\n%s\n", "skywire command line interface") + fmt.Printf("\n## %s\n", RootCmd.Use) + fmt.Printf("\n```\n") + RootCmd.Help() //nolint + fmt.Printf("\n```\n") + fmt.Printf("\n## %s\n", "global flags") + fmt.Printf("\n%s\n", "The skywire-cli interacts with the running visor via rpc calls. By default the rpc server is available on localhost:3435. The rpc address and port the visor is using may be changed in the config file, once generated.") + + fmt.Printf("\n%s\n", "It is not recommended to expose the rpc server on the local network. Exposing the rpc allows unsecured access to the machine over the local network") + fmt.Printf("\n```\n") + fmt.Printf("\n%s\n", "Global Flags:") + fmt.Printf("\n%s\n", " --rpc string RPC server address (default \"localhost:3435\")") + fmt.Printf("\n%s\n", " --json bool print output as json") + fmt.Printf("\n```\n") + } + for _, k := range j.Commands() { + use = strings.Split(j.Use, " ")[0] + " " + strings.Split(k.Use, " ")[0] + fmt.Printf("\n#### %s\n", use) + fmt.Printf("\n```\n") + k.Help() //nolint + fmt.Printf("\n```\n") + if k.Name() == "survey" { + fmt.Printf("\n```\n") + _, err = script.Exec("sudo " + os.Args[0] + ` survey`).Stdout() //nolint + if err != nil { + fmt.Println(err.Error()) + } + fmt.Printf("\n```\n") + } + for _, l := range k.Commands() { + use = strings.Split(j.Use, " ")[0] + " " + strings.Split(k.Use, " ")[0] + " " + strings.Split(l.Use, " ")[0] + fmt.Printf("\n##### %s\n", use) + fmt.Printf("\n```\n") + l.Help() //nolint + fmt.Printf("\n```\n") + if l.Name() == "gen" { + fmt.Printf("\n##### Example for package / msi\n") + fmt.Printf("\n```\n") + fmt.Printf("$ skywire cli config gen -bpirxn\n") + _, err = script.Exec(os.Args[0] + ` cli config gen -bpirxn`).Stdout() //nolint + if err != nil { + fmt.Println(err.Error()) + } + fmt.Printf("\n```\n") + } + for _, m := range l.Commands() { + use = strings.Split(j.Use, " ")[0] + " " + strings.Split(k.Use, " ")[0] + " " + strings.Split(l.Use, " ")[0] + " " + strings.Split(m.Use, " ")[0] + fmt.Printf("\n###### %s\n", use) + fmt.Printf("\n```\n") + m.Help() //nolint + fmt.Printf("\n```\n") + for _, n := range m.Commands() { + use = strings.Split(j.Use, " ")[0] + " " + strings.Split(k.Use, " ")[0] + " " + strings.Split(l.Use, " ")[0] + " " + strings.Split(m.Use, " ")[0] + " " + strings.Split(n.Use, " ")[0] + fmt.Printf("\n###### %s\n", use) + fmt.Printf("\n```\n") + m.Help() //nolint + fmt.Printf("\n```\n") + for _, o := range n.Commands() { + use = strings.Split(j.Use, " ")[0] + " " + strings.Split(k.Use, " ")[0] + " " + strings.Split(l.Use, " ")[0] + " " + strings.Split(m.Use, " ")[0] + " " + strings.Split(n.Use, " ")[0] + " " + strings.Split(o.Use, " ")[0] + fmt.Printf("\n###### %s\n", use) + fmt.Printf("\n```\n") + m.Help() //nolint + fmt.Printf("\n```\n") + } + } + } + } + } + } + }, +} + func main() { cc.Init(&cc.Config{ - RootCmd: rootCmd, + RootCmd: RootCmd, Headings: cc.HiBlue + cc.Bold, Commands: cc.HiBlue + cc.Bold, CmdShortDescr: cc.HiBlue, @@ -57,8 +392,7 @@ func main() { NoExtraNewlines: true, NoBottomNewline: true, }) - - if err := rootCmd.Execute(); err != nil { + if err := RootCmd.Execute(); err != nil { fmt.Println(err) } } diff --git a/cmd/skywirevisormobile/android/README.md b/cmd/skywirevisormobile/android/README.md new file mode 100644 index 000000000..59bacc615 --- /dev/null +++ b/cmd/skywirevisormobile/android/README.md @@ -0,0 +1,66 @@ +# Skywire VPN draft (Android) + +## Prerequisites +This project uses Skywire mobile library (`skywiremob`) to use all the needed Skywire infrastructure. In order to build +and use this library, one needs to install `gomobile` (https://github.com/golang/mobile). + +## Building and Running +To build the library you need to use `gomobile`. Main `Makefile` already contains target `build-android` which +can be used. +IMPORTANT: regardless of go modules and other great stuff done by the Go team, in order to +use `gomobile` you need to put Skywire code according to `GOPATH`. Otherwise you'll get all kinds of errors. +The output file (`.aar` for Android) may be used straight from the mobile app code. + +## Skywire Mobile API +- `PrintString(string)`: Logs string argument using info log level. May be useful to use it instead of standard logging features of mobile apps for debugging. +All the output strings are prefixed with `GoLog`, so printing logs with this func one may grep all the logs both from `skywiremob` +internal and mobile application; +- `IsPKValid(string) string`: Checks if passed pub key is valid. Returns non-empty string with error in case of failure; +- `GetMTU() int`: Gets VPN connection MTU; +- `GetTUNIPPrefix() int`: Gets netmask prefix of TUN IP address; +- `IsVPNReady() bool`: Checks whether VPN client is ready on the Go side. Once it is, the mobile application is free to start +forwarding packets. Starts returning `true` after the `ServerVPN` call; +- `PrepareVisor() string`: Creates and runs visor instance. Returns non-empty string with error in case of failure; +- `NextDmsgSocket() int`: Returns file descriptor of the Dmsg socket. There may be more than one socket in use by the dmsg client, so +this function should be called repeatedly until next call returns 0. +- `PrepareVPNClient(string, string) string`: Creates VPN client instance. First string argument is remote VPN server pub key, second one is passcode to +authenticate within the server. Returns non-empty string with error in case of failure; +- `ShakeHands() string`: Requires `PrepareVPNClient` to be called first. Performs handshake between the client and the server. +Returns non-empty string with error in case of failure; +- `TUNIP() string`: Requires `ShakeHands` to be called first. Returns the assigned TUN IP; +- `TUNGateway() string`: Requires `ShakeHands` to be called first. Returns the assigned TUN gateway; +- `StopVisor() string`: Stops currently running visor. Returns non-empty string with error in case of failure; +- `SetMobileAppAddr(string)`: Passes address of the UDP connection opened on the mobile application side; +- `ServeVPN()`: Starts off the goroutine serving VPN connection. After this call `IsVPNReady` starts returning `true`; +- `StartListeningUDP() string`: Opens UDP listener on the Go side. Returns non-empty string with error in case of failure; +- `IsVisorStarting() bool`: Checks if visor is starting. Will get `false` when it's fully functional; +- `IsVisorRunning() bool`: Checks if visor is running. Will get `true` whn visor is fully functional; +- `WaitVisorReady() string`: Blocks until visor gets fully initialized. Returns non-empty error string in case of failure; +- `StopVPNClient`: Stops VPN client without stopping visor itself; +- `StopListeningUDP`: Closes UDP socket; +- `VPNBandwidthSent`: Returns amount of bandwidth sent over VPN (bytes); +- `VPNBandwidthReceived`: Returns amount of bandwidth received over VPN (bytes); +- `VPNLatency`: Returns latency (ms); +- `VPNThroughput`: Returns throughput (bytes/s). + + +## Mobile/Go Communication +API may seem a bit complicated at first. Currently tested for Android devices, should be used with caution on iOS. +Mobile app communicates with the Go part via UDP. All the packets are sent to the Go part via UDP and then get resent +to the Skywire network. + +To setup the Go side properly you need to call at least: +- `PrepareVisor` to run the visor; +- `PrepareVPNClient` to run the VPN client; +- `ShakeHands` to perform handshake with the server; +- `StartListeningUDP` to open the UDP listener on the Go side; +- `ServeVPN` to start forwarding traffic. + +All other calls should be done as needed. + +### Android +Consult this page: https://developer.android.com/guide/topics/connectivity/vpn + +In the example mobile app communicates with the remote server via `DatagramChannel`. Socket opened to the server gets protected +with the `protect` method. We do the same here. But instead of a remote server we open the `DatagramChannel` to the Go part of the app. +We protect not only the tunnel socket, but also we need to protect all the sockets used for `Dmsg` communication to let traffic go back and forth freely. diff --git a/cmd/skywirevisormobile/android/app/build.gradle b/cmd/skywirevisormobile/android/app/build.gradle new file mode 100644 index 000000000..debee173b --- /dev/null +++ b/cmd/skywirevisormobile/android/app/build.gradle @@ -0,0 +1,61 @@ +/* + * Copyright 2015 The Go Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ +apply plugin: 'com.android.application' + +repositories { + flatDir { + dirs '.' + } + maven { url 'https://jitpack.io' } +} + +android { + compileSdkVersion 29 + + defaultConfig { + applicationId "com.skywire.skycoin.vpn" + minSdkVersion 21 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + + +dependencies { + // Appcompat. + implementation "androidx.appcompat:appcompat:1.2.0" + implementation 'com.google.android.material:material:1.2.1' + implementation "androidx.preference:preference:1.1.1" + implementation "androidx.recyclerview:recyclerview:1.1.0" + implementation "androidx.viewpager2:viewpager2:1.0.0" + + // Skywire lib. + implementation(name:'skywire', ext:'aar') + + // RxJava. + implementation 'io.reactivex.rxjava3:rxandroid:3.0.0' + implementation 'io.reactivex.rxjava3:rxjava:3.0.0' + + // Retrofit. + implementation 'com.google.code.gson:gson:2.8.5' + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.retrofit2:converter-gson:2.9.0' + implementation 'com.squareup.retrofit2:adapter-rxjava3:2.9.0' + implementation 'com.squareup.retrofit2:converter-scalars:2.9.0' + + // MPAndroidChart. + implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0' +} diff --git a/cmd/skywirevisormobile/android/app/src/main/AndroidManifest.xml b/cmd/skywirevisormobile/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..f0a71f93f --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/App.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/App.java new file mode 100644 index 000000000..e3627d5d3 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/App.java @@ -0,0 +1,118 @@ +package com.skywire.skycoin.vpn; + +import android.app.Activity; +import android.app.Application; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; +import android.os.Build; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.skywire.skycoin.vpn.helpers.HelperFunctions; +import com.skywire.skycoin.vpn.helpers.Notifications; +import com.skywire.skycoin.vpn.vpn.VPNCoordinator; + +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Class for the main app instance. + */ +public class App extends Application { + /** + * Class used internally to know when there are activities being displayed. + */ + private static class ActivityLifecycleCallback implements Application.ActivityLifecycleCallbacks { + + // How many activities are being shown. + private static int foregroundActivities = 0; + + // Functions for knowing when activities start and stop being shown. + @Override + public void onActivityResumed(@NonNull final Activity activity) { foregroundActivities++; } + @Override + public void onActivityStopped(@NonNull final Activity activity) { foregroundActivities--; } + + /** + * Returns if there is at least one activity being displayed. + */ + public static boolean isApplicationInForeground() { return foregroundActivities > 0; } + + // Other functions needed by the interface. + @Override + public void onActivityPaused(@NonNull Activity activity) { } + @Override + public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) { } + @Override + public void onActivityDestroyed(@NonNull Activity activity) { } + @Override + public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) { } + @Override + public void onActivityStarted(@NonNull Activity activity) { } + } + + /** + * Reference to the current app instance. + */ + private static Context appContext; + + @Override + public void onCreate() { + super.onCreate(); + // Save the current app instance. + appContext = this; + + // Ensure the singleton is initialized early. + VPNCoordinator.getInstance(); + + // Create the notification channels, but only on API 26+ because + // the NotificationChannel class is new and not in the support library + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // Channel for the VPN service state updates. + NotificationChannel stateChannel = new NotificationChannel( + Notifications.NOTIFICATION_CHANNEL_ID, + getString(R.string.general_app_name), + NotificationManager.IMPORTANCE_DEFAULT + ); + stateChannel.setDescription(getString(R.string.general_notification_channel_description)); + stateChannel.setSound(null,null); + NotificationManager notificationManager = getSystemService(NotificationManager.class); + notificationManager.createNotificationChannel(stateChannel); + + // Channel for alerts. + NotificationChannel alertsChannel = new NotificationChannel( + Notifications.ALERT_NOTIFICATION_CHANNEL_ID, + getString(R.string.general_alert_notification_name), + NotificationManager.IMPORTANCE_HIGH + ); + alertsChannel.setDescription(getString(R.string.general_alert_notification_channel_description)); + notificationManager.createNotificationChannel(alertsChannel); + } + + // Code for precessing errors which were not caught by the normal error management + // procedures RxJava has. This prevents the app to be closed by unexpected errors, mainly + // code trying to report events in closed observables. + RxJavaPlugins.setErrorHandler(throwable -> { + HelperFunctions.logError("ERROR INSIDE RX: ", throwable); + }); + + // Detect when activities are started and stopped. + registerActivityLifecycleCallbacks(new ActivityLifecycleCallback()); + } + + /** + * Gets the current app context. + */ + public static Context getContext(){ + return appContext; + } + + /** + * Gets if the UI is being displayed. + */ + public static boolean displayingUI(){ + return ActivityLifecycleCallback.isApplicationInForeground(); + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/Receiver.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/Receiver.java new file mode 100644 index 000000000..99aae2322 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/Receiver.java @@ -0,0 +1,23 @@ +package com.skywire.skycoin.vpn; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import com.skywire.skycoin.vpn.vpn.VPNCoordinator; +import com.skywire.skycoin.vpn.vpn.VPNGeneralPersistentData; + +/** + * Class for receiving the system boot event broadcast. + */ +public class Receiver extends BroadcastReceiver { + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) { + // If the option for starting the service automatically after booting the OS is active + // and the service is not currently running, start the service. + if (VPNGeneralPersistentData.getStartOnBoot() && !VPNCoordinator.getInstance().isServiceRunning()) { + VPNCoordinator.getInstance().activateAutostart(); + } + } + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/apps/AppListButton.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/apps/AppListButton.java new file mode 100644 index 000000000..02f8ae5bf --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/apps/AppListButton.java @@ -0,0 +1,124 @@ +package com.skywire.skycoin.vpn.activities.apps; + +import android.content.Context; +import android.content.pm.ResolveInfo; +import android.graphics.drawable.RippleDrawable; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.widget.CheckBox; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.extensible.ListButtonBase; + +public class AppListButton extends ListButtonBase implements View.OnTouchListener { + public static final float APROX_HEIGHT_DP = 55; + + private FrameLayout mainLayout; + private LinearLayout internalLayout; + private ImageView imageIcon; + private FrameLayout layoutSeparator; + private TextView textAppName; + private CheckBox checkSelected; + private View separator; + + private RippleDrawable rippleDrawable; + + private String appPackageName; + + public AppListButton(Context context) { + super(context); + } + public AppListButton(Context context, AttributeSet attrs) { + super(context, attrs); + } + public AppListButton(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void Initialize (Context context, AttributeSet attrs) { + LayoutInflater inflater = (LayoutInflater)context.getSystemService (Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_app_list_item, this, true); + + mainLayout = this.findViewById (R.id.mainLayout); + internalLayout = this.findViewById (R.id.internalLayout); + imageIcon = this.findViewById (R.id.imageIcon); + layoutSeparator = this.findViewById (R.id.layoutSeparator); + textAppName = this.findViewById (R.id.textAppName); + checkSelected = this.findViewById (R.id.checkSelected); + separator = this.findViewById (R.id.separator); + + rippleDrawable = (RippleDrawable) mainLayout.getBackground(); + setOnTouchListener(this); + setViewForCheckingClicks(this); + + setUseBigFastClickPrevention(false); + } + + public void setSeparatorVisibility(boolean visible) { + if (visible) { + separator.setVisibility(VISIBLE); + } else { + separator.setVisibility(GONE); + } + } + + public void changeData(ResolveInfo appData) { + if (appData != null) { + appPackageName = appData.activityInfo.packageName; + imageIcon.setImageDrawable(appData.activityInfo.loadIcon(this.getContext().getPackageManager())); + textAppName.setText(appData.activityInfo.loadLabel(this.getContext().getPackageManager())); + imageIcon.setVisibility(VISIBLE); + layoutSeparator.setVisibility(VISIBLE); + setVisibility(VISIBLE); + } else { + setVisibility(INVISIBLE); + } + } + + public void changeData(String appPackageName) { + imageIcon.setVisibility(GONE); + layoutSeparator.setVisibility(GONE); + if (appPackageName != null) { + this.appPackageName = appPackageName; + textAppName.setText(appPackageName); + setVisibility(VISIBLE); + } else { + setVisibility(INVISIBLE); + } + } + + public String getAppPackageName() { + return appPackageName; + } + + public void setChecked(boolean checked) { + checkSelected.setChecked(checked); + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + + if (enabled) { + internalLayout.setAlpha(1f); + } else { + internalLayout.setAlpha(0.5f); + } + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (rippleDrawable != null) { + rippleDrawable.setHotspot(event.getX(), event.getY()); + } + + return false; + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/apps/AppListOptionButton.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/apps/AppListOptionButton.java new file mode 100644 index 000000000..52431579f --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/apps/AppListOptionButton.java @@ -0,0 +1,62 @@ +package com.skywire.skycoin.vpn.activities.apps; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.widget.RadioButton; +import android.widget.TextView; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.controls.BoxRowLayout; +import com.skywire.skycoin.vpn.extensible.ListButtonBase; +import com.skywire.skycoin.vpn.helpers.BoxRowTypes; + +public class AppListOptionButton extends ListButtonBase { + private BoxRowLayout mainLayout; + private TextView textOption; + private TextView textDescription; + private RadioButton radioSelected; + + public AppListOptionButton(Context context) { + super(context); + } + + @Override + protected void Initialize (Context context, AttributeSet attrs) { + LayoutInflater inflater = (LayoutInflater)context.getSystemService (Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_app_list_selection_option, this, true); + + mainLayout = this.findViewById (R.id.mainLayout); + textOption = this.findViewById (R.id.textOption); + textDescription = this.findViewById (R.id.textDescription); + radioSelected = this.findViewById (R.id.radioSelected); + + radioSelected.setChecked(false); + + setClickableBoxView(mainLayout); + } + + public void setBoxRowType(BoxRowTypes type) { + mainLayout.setType(type); + } + + public void changeData(int textResource, int descriptionResource) { + textOption.setText(textResource); + textDescription.setText(descriptionResource); + } + + public void setChecked(boolean checked) { + radioSelected.setChecked(checked); + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + + if (enabled) { + setAlpha(1f); + } else { + setAlpha(0.5f); + } + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/apps/AppListRow.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/apps/AppListRow.java new file mode 100644 index 000000000..1e6888c08 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/apps/AppListRow.java @@ -0,0 +1,115 @@ +package com.skywire.skycoin.vpn.activities.apps; + +import android.content.Context; +import android.content.pm.ResolveInfo; +import android.view.LayoutInflater; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.controls.BoxRowLayout; +import com.skywire.skycoin.vpn.extensible.ClickWithIndexEvent; +import com.skywire.skycoin.vpn.helpers.BoxRowTypes; + +public class AppListRow extends FrameLayout implements ClickWithIndexEvent { + private BoxRowLayout mainLayout; + private LinearLayout buttonsContainer; + + private AppListButton[] buttons; + private ClickWithIndexEvent clickListener; + + public AppListRow(Context context, int buttonsPerRow) { + super(context); + + LayoutInflater inflater = (LayoutInflater)context.getSystemService (Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_app_list_row, this, true); + + mainLayout = this.findViewById(R.id.mainLayout); + buttonsContainer = this.findViewById(R.id.buttonsContainer); + + buttonsContainer.setClipToOutline(true); + + buttons = new AppListButton[buttonsPerRow]; + LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT, 1f); + for (int i = 0; i < buttonsPerRow; i++) { + AppListButton btn = new AppListButton(context); + btn.setLayoutParams(layoutParams); + btn.setClickWithIndexEventListener(this); + buttons[i] = btn; + buttonsContainer.addView(btn); + } + } + + public void setIndex(int index) { + for (int i = 0; i < buttons.length; i++) { + buttons[i].setIndex(index + i); + } + } + + public void setClickWithIndexEventListener(ClickWithIndexEvent listener) { + clickListener = listener; + } + + public void changeData(ResolveInfo[] appData) { + for (int i = 0; i < buttons.length; i++) { + buttons[i].changeData(appData[i]); + } + } + + public void changeData(String[] appPackageName) { + for (int i = 0; i < buttons.length; i++) { + buttons[i].changeData(appPackageName[i]); + } + } + + public void setBoxRowType(BoxRowTypes type) { + mainLayout.setType(type); + + boolean showSeparator = true; + if (type == BoxRowTypes.TOP) { + buttonsContainer.setBackgroundResource(R.drawable.internal_box_row_rounded_box_1); + } else if (type == BoxRowTypes.MIDDLE) { + buttonsContainer.setBackgroundResource(R.drawable.internal_box_row_rounded_box_2); + } else if (type == BoxRowTypes.BOTTOM) { + buttonsContainer.setBackgroundResource(R.drawable.internal_box_row_rounded_box_3); + showSeparator = false; + } else { + buttonsContainer.setBackgroundResource(R.drawable.internal_box_row_rounded_box_4); + showSeparator = false; + } + + for (int i = 0; i < buttons.length; i++) { + buttons[i].setSeparatorVisibility(showSeparator); + } + } + + public void setChecked(String packageName, boolean checked) { + for (int i = 0; i < buttons.length; i++) { + if (buttons[i].getAppPackageName() != null && buttons[i].getAppPackageName().equals(packageName)) { + buttons[i].setChecked(checked); + } + } + } + + public void setChecked(boolean[] checked) { + for (int i = 0; i < buttons.length; i++) { + buttons[i].setChecked(checked[i]); + } + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + + for (int i = 0; i < buttons.length; i++) { + buttons[i].setEnabled(enabled); + } + } + + @Override + public void onClickWithIndex(int index, Void data) { + if (clickListener != null) { + clickListener.onClickWithIndex(index, data); + } + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/apps/AppListSeparator.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/apps/AppListSeparator.java new file mode 100644 index 000000000..6c51f7183 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/apps/AppListSeparator.java @@ -0,0 +1,29 @@ +package com.skywire.skycoin.vpn.activities.apps; + +import android.content.Context; +import android.view.LayoutInflater; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.helpers.HelperFunctions; + +public class AppListSeparator extends LinearLayout { + private TextView textTitle; + + public AppListSeparator(Context context) { + super(context); + + LayoutInflater inflater = (LayoutInflater)context.getSystemService (Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_app_list_separator, this, true); + + textTitle = this.findViewById (R.id.textTitle); + + int tabletExtraHorizontalPadding = HelperFunctions.getTabletExtraHorizontalPadding(getContext()); + setPadding(tabletExtraHorizontalPadding, 0, tabletExtraHorizontalPadding, 0); + } + + public void changeTitle(int title) { + textTitle.setText(title); + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/apps/AppsActivity.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/apps/AppsActivity.java new file mode 100644 index 000000000..c89673f17 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/apps/AppsActivity.java @@ -0,0 +1,50 @@ +package com.skywire.skycoin.vpn.activities.apps; + +import android.os.Bundle; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.helpers.HelperFunctions; + +public class AppsActivity extends AppCompatActivity implements AppsAdapter.AppListChangedListener { + public static final String READ_ONLY_EXTRA = "ReadOnly"; + + private RecyclerView recycler; + + private boolean readOnly; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_app_list); + + recycler = findViewById(R.id.recycler); + + readOnly = getIntent().getBooleanExtra(READ_ONLY_EXTRA, false); + + LinearLayoutManager layoutManager = new LinearLayoutManager(this); + recycler.setLayoutManager(layoutManager); + // This could be useful in the future. + // recycler.setHasFixedSize(true); + + AppsAdapter adapter = new AppsAdapter(this, readOnly); + adapter.setAppListChangedEventListener(this); + recycler.setAdapter(adapter); + } + + @Override + protected void onResume() { + super.onResume(); + if (!readOnly) { + HelperFunctions.closeActivityIfServiceRunning(this); + } + } + + @Override + public boolean onAppListChanged() { + return !HelperFunctions.closeActivityIfServiceRunning(this); + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/apps/AppsAdapter.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/apps/AppsAdapter.java new file mode 100644 index 000000000..72defac1c --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/apps/AppsAdapter.java @@ -0,0 +1,339 @@ +package com.skywire.skycoin.vpn.activities.apps; + +import android.content.Context; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.helpers.BoxRowTypes; +import com.skywire.skycoin.vpn.extensible.ClickWithIndexEvent; +import com.skywire.skycoin.vpn.helpers.Globals; +import com.skywire.skycoin.vpn.helpers.HelperFunctions; +import com.skywire.skycoin.vpn.extensible.ListViewHolder; +import com.skywire.skycoin.vpn.vpn.VPNGeneralPersistentData; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +public class AppsAdapter extends RecyclerView.Adapter> implements ClickWithIndexEvent { + public interface AppListChangedListener { + boolean onAppListChanged(); + } + + private final int installedAppsIndexExtra = 10; + private final int uninstalledAppsIndexExtra = 1000000; + + private Context context; + private List appList; + private List uninstalledApps; + private AppListChangedListener appListChangedListener; + + private HashSet selectedApps; + private Globals.AppFilteringModes selectedOption; + + private int[] optionTexts = new int[3]; + private int[] optionDescriptions = new int[3]; + private ArrayList optionButtons = new ArrayList<>(); + private ArrayList appRows = new ArrayList<>(); + + private ArrayList premadeRows = new ArrayList<>(); + private int lastUsedPremadeRowIndex = 0; + + private int elementsPerRow = 1; + + private boolean readOnly; + + public AppsAdapter(Context context, boolean readOnly) { + this.context = context; + this.readOnly = readOnly; + + selectedApps = VPNGeneralPersistentData.getAppList(new HashSet<>()); + changeSelectedOption(VPNGeneralPersistentData.getAppsSelectionMode()); + + appList = HelperFunctions.getDeviceAppsList(); + + HashSet filteredApps = HelperFunctions.filterAvailableApps(selectedApps); + if (filteredApps.size() != selectedApps.size()) { + uninstalledApps = new ArrayList<>(); + + for (String app : selectedApps) { + if (!filteredApps.contains(app)) { + uninstalledApps.add(app); + } + } + } + + optionTexts[0] = R.string.tmp_select_apps_protect_all_button; + optionTexts[1] = R.string.tmp_select_apps_protect_selected_button; + optionTexts[2] = R.string.tmp_select_apps_unprotect_selected_button; + + optionDescriptions[0] = R.string.tmp_select_apps_protect_all_button_desc; + optionDescriptions[1] = R.string.tmp_select_apps_protect_selected_button_desc; + optionDescriptions[2] = R.string.tmp_select_apps_unprotect_selected_button_desc; + + int screenWidthInDP = (int)(Resources.getSystem().getDisplayMetrics().widthPixels / context.getResources().getDisplayMetrics().density); + elementsPerRow = Math.max(screenWidthInDP / 360, 1); + + int screenHeightInDP = (int)(Resources.getSystem().getDisplayMetrics().heightPixels / context.getResources().getDisplayMetrics().density); + int aproxRowsToFillScreen = (int)Math.ceil((screenHeightInDP / AppListButton.APROX_HEIGHT_DP) * 1.3); + + for (int i = 0; i < aproxRowsToFillScreen; i++) { + premadeRows.add(createNewRow()); + } + } + + public void setAppListChangedEventListener(AppListChangedListener listener) { + appListChangedListener = listener; + } + + private int getInstalledAppsRowsCount() { + return (int)Math.ceil((double)appList.size() / (double)elementsPerRow); + } + + private int getUninstalledAppsRowsCount() { + if (uninstalledApps == null) { + return 0; + } + + return (int)Math.ceil((double)uninstalledApps.size() / (double)elementsPerRow); + } + + @Override + public int getItemViewType(int position) { + if (position == 0 || position == 4 || position == 5 + getInstalledAppsRowsCount()) { + return 2; + } + + if (position < 4) { + return 0; + } + + return 1; + } + + @NonNull + @Override + public ListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + if (viewType == 0) { + AppListOptionButton view = new AppListOptionButton(context); + view.setClickWithIndexEventListener(this); + optionButtons.add(view); + + if (readOnly) { + view.setEnabled(false); + } + + return new ListViewHolder<>(view); + } else if (viewType == 1) { + AppListRow view; + + if (lastUsedPremadeRowIndex < premadeRows.size()) { + view = premadeRows.get(lastUsedPremadeRowIndex); + lastUsedPremadeRowIndex += 1; + } else { + view = createNewRow(); + } + + return new ListViewHolder<>(view); + } + + AppListSeparator view = new AppListSeparator(context); + + return new ListViewHolder<>(view); + } + + private AppListRow createNewRow() { + AppListRow view = new AppListRow(context, elementsPerRow); + view.setClickWithIndexEventListener(this); + view.setEnabled(selectedOption != Globals.AppFilteringModes.PROTECT_ALL); + appRows.add(view); + + if (readOnly) { + view.setEnabled(false); + } + + return view; + } + + @Override + public void onBindViewHolder(@NonNull ListViewHolder holder, int position) { + if (holder.getItemViewType() == 0) { + boolean showChecked = false; + if (position == 1 && selectedOption == Globals.AppFilteringModes.PROTECT_ALL) { showChecked = true; } + if (position == 2 && selectedOption == Globals.AppFilteringModes.PROTECT_SELECTED) { showChecked = true; } + if (position == 3 && selectedOption == Globals.AppFilteringModes.IGNORE_SELECTED) { showChecked = true; } + + ((AppListOptionButton)(holder.itemView)).setIndex(position); + ((AppListOptionButton)(holder.itemView)).changeData(optionTexts[position - 1], optionDescriptions[position - 1]); + ((AppListOptionButton)(holder.itemView)).setChecked(showChecked); + + if (position == 1) { + ((AppListOptionButton)holder.itemView).setBoxRowType(BoxRowTypes.TOP); + } else if (position == 2) { + ((AppListOptionButton)holder.itemView).setBoxRowType(BoxRowTypes.MIDDLE); + } else { + ((AppListOptionButton)holder.itemView).setBoxRowType(BoxRowTypes.BOTTOM); + } + + return; + } else if (holder.getItemViewType() == 2) { + if (position == 0) { + ((AppListSeparator)holder.itemView).changeTitle(R.string.tmp_select_apps_mode_title); + } else if (position == 4) { + if (this.uninstalledApps != null) { + ((AppListSeparator) holder.itemView).changeTitle(R.string.tmp_select_apps_installed_apps_title); + } else { + ((AppListSeparator) holder.itemView).changeTitle(R.string.tmp_select_apps_apps_title); + } + } else { + ((AppListSeparator)holder.itemView).changeTitle(R.string.tmp_select_apps_uninstalled_apps_title); + } + + return; + } + + int initialInstalledAppsRowIndex = 5; + if (position < initialInstalledAppsRowIndex + getInstalledAppsRowsCount()) { + int rowIndex = (position - initialInstalledAppsRowIndex); + + ResolveInfo[] dataForRow = new ResolveInfo[elementsPerRow]; + boolean[] checkedListForRow = new boolean[elementsPerRow]; + for (int i = 0; i < elementsPerRow; i++){ + int appIndex = (rowIndex * elementsPerRow) + i; + if (appIndex < appList.size()) { + dataForRow[i] = appList.get(appIndex); + checkedListForRow[i] = selectedApps.contains(appList.get(appIndex).activityInfo.packageName); + } + } + + ((AppListRow) (holder.itemView)).setIndex(installedAppsIndexExtra + (rowIndex * elementsPerRow)); + ((AppListRow) (holder.itemView)).changeData(dataForRow); + ((AppListRow) (holder.itemView)).setChecked(checkedListForRow); + + if (getInstalledAppsRowsCount() == 1) { + ((AppListRow)holder.itemView).setBoxRowType(BoxRowTypes.SINGLE); + } else if (rowIndex == 0) { + ((AppListRow)holder.itemView).setBoxRowType(BoxRowTypes.TOP); + } else if (rowIndex == getInstalledAppsRowsCount() - 1) { + ((AppListRow)holder.itemView).setBoxRowType(BoxRowTypes.BOTTOM); + } else { + ((AppListRow)holder.itemView).setBoxRowType(BoxRowTypes.MIDDLE); + } + } else { + int initialUninstalledAppsRowIndex = initialInstalledAppsRowIndex + getInstalledAppsRowsCount() + 1; + int rowIndex = (position - initialUninstalledAppsRowIndex); + + String[] dataForRow = new String[elementsPerRow]; + boolean[] checkedListForRow = new boolean[elementsPerRow]; + for (int i = 0; i < elementsPerRow; i++){ + int appIndex = (rowIndex * elementsPerRow) + i; + if (appIndex < uninstalledApps.size()) { + dataForRow[i] = uninstalledApps.get(appIndex); + checkedListForRow[i] = selectedApps.contains(uninstalledApps.get(appIndex)); + } + } + + ((AppListRow) (holder.itemView)).setIndex(uninstalledAppsIndexExtra + (rowIndex * elementsPerRow)); + ((AppListRow) (holder.itemView)).changeData(dataForRow); + ((AppListRow) (holder.itemView)).setChecked(checkedListForRow); + + if (getUninstalledAppsRowsCount() == 1) { + ((AppListRow)holder.itemView).setBoxRowType(BoxRowTypes.SINGLE); + } else if (rowIndex == 0) { + ((AppListRow)holder.itemView).setBoxRowType(BoxRowTypes.TOP); + } else if (rowIndex == getUninstalledAppsRowsCount() - 1) { + ((AppListRow)holder.itemView).setBoxRowType(BoxRowTypes.BOTTOM); + } else { + ((AppListRow)holder.itemView).setBoxRowType(BoxRowTypes.MIDDLE); + } + } + } + + @Override + public int getItemCount() { + int result = 3 + 2 + getInstalledAppsRowsCount(); + + if (getUninstalledAppsRowsCount() > 0) { + result += 1 + getUninstalledAppsRowsCount(); + } + + return result; + } + + @Override + public void onClickWithIndex(int index, Void data) { + if (appListChangedListener != null) { + if (!appListChangedListener.onAppListChanged()) { + return; + } + } + + if (index < installedAppsIndexExtra) { + if (index == 1) { + changeSelectedOption(Globals.AppFilteringModes.PROTECT_ALL); + } else if (index == 2) { + changeSelectedOption(Globals.AppFilteringModes.PROTECT_SELECTED); + } else if (index == 3) { + changeSelectedOption(Globals.AppFilteringModes.IGNORE_SELECTED); + } + } else { + processAppClicked(index); + } + } + + private void changeSelectedOption(Globals.AppFilteringModes option) { + if (option != selectedOption) { + if (option == Globals.AppFilteringModes.PROTECT_ALL) { + for (AppListRow row : appRows) { + row.setEnabled(false); + } + } else if (selectedOption == Globals.AppFilteringModes.PROTECT_ALL) { + for (AppListRow row : appRows) { + row.setEnabled(true); + } + } + + selectedOption = option; + VPNGeneralPersistentData.setAppsSelectionMode(selectedOption); + + for (AppListOptionButton optionButton : optionButtons) { + optionButton.setChecked( + (optionButton.getIndex() == 1 && selectedOption == Globals.AppFilteringModes.PROTECT_ALL) || + (optionButton.getIndex() == 2 && selectedOption == Globals.AppFilteringModes.PROTECT_SELECTED) || + (optionButton.getIndex() == 3 && selectedOption == Globals.AppFilteringModes.IGNORE_SELECTED) + ); + } + } + } + + private void processAppClicked(int index) { + String app; + + if (index < uninstalledAppsIndexExtra) { + app = appList.get(index - installedAppsIndexExtra).activityInfo.packageName; + } else { + app = uninstalledApps.get(index - uninstalledAppsIndexExtra); + } + + boolean showAppChecked; + if (selectedApps.contains(app)) { + selectedApps.remove(app); + showAppChecked = false; + } else { + selectedApps.add(app); + showAppChecked = true; + } + + for (AppListRow row : appRows) { + row.setChecked(app, showAppChecked); + } + + VPNGeneralPersistentData.setAppList(selectedApps); + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/index/IndexActivity.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/index/IndexActivity.java new file mode 100644 index 000000000..dfa267d28 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/index/IndexActivity.java @@ -0,0 +1,185 @@ +package com.skywire.skycoin.vpn.activities.index; + +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.viewpager2.widget.ViewPager2; + +import com.google.android.material.tabs.TabLayout; +import com.google.android.material.tabs.TabLayoutMediator; +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.controls.TabletTopBar; +import com.skywire.skycoin.vpn.controls.TopBar; +import com.skywire.skycoin.vpn.controls.TopTab; +import com.skywire.skycoin.vpn.extensible.ClickWithIndexEvent; +import com.skywire.skycoin.vpn.helpers.HelperFunctions; +import com.skywire.skycoin.vpn.vpn.VPNCoordinator; + +public class IndexActivity extends AppCompatActivity implements IndexPageAdapter.RequestTabListener, ClickWithIndexEvent { + private ImageView imageBackground; + private ImageView imageTopBarShadow; + private ViewPager2 pager; + private TopBar topBar; + private TabletTopBar tabletTopBar; + private TabLayout tabs; + + private TabLayoutMediator tabLayoutMediator; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_index); + + imageBackground = findViewById(R.id.imageBackground); + imageTopBarShadow = findViewById(R.id.imageTopBarShadow); + pager = findViewById(R.id.pager); + topBar = findViewById(R.id.topBar); + tabletTopBar = findViewById(R.id.tabletTopBar); + tabs = findViewById(R.id.tabs); + + if (HelperFunctions.showBackgroundForVerticalScreen()) { + imageBackground.setVisibility(View.GONE); + } + + IndexPageAdapter adapter = new IndexPageAdapter(this); + adapter.setRequestTabListener(this); + pager.setAdapter(adapter); + + tabLayoutMediator = new TabLayoutMediator(tabs, pager, (tab, position) -> { + if (position == 0) { + tab.setCustomView(new TopTab(this, R.string.tmp_status_page_title)); + } else if (position == 1) { + tab.setCustomView(new TopTab(this, R.string.tmp_select_server_title)); + } else { + tab.setCustomView(new TopTab(this, R.string.tmp_options_title)); + } + + if (position != 0) { + tab.getCustomView().setAlpha(0.4f); + } + }); + tabLayoutMediator.attach(); + + pager.setOffscreenPageLimit(3); + + if (HelperFunctions.getWidthType(this) == HelperFunctions.WidthTypes.SMALL) { + tabletTopBar.setVisibility(View.GONE); + tabletTopBar.close(); + + tabs.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { + @Override + public void onTabSelected(TabLayout.Tab tab) { + tab.getCustomView().setAlpha(1f); + } + @Override + public void onTabUnselected(TabLayout.Tab tab) { + tab.getCustomView().setAlpha(0.4f); + } + @Override + public void onTabReselected(TabLayout.Tab tab) { } + }); + } else { + topBar.setVisibility(View.GONE); + tabs.setVisibility(View.GONE); + imageTopBarShadow.setVisibility(View.GONE); + + FrameLayout.LayoutParams params = (FrameLayout.LayoutParams)imageBackground.getLayoutParams(); + params.topMargin = 0; + imageBackground.setLayoutParams(params); + + params = (FrameLayout.LayoutParams)pager.getLayoutParams(); + params.topMargin = (int)Math.round(getResources().getDimension(R.dimen.tablet_top_bar_height)); + pager.setLayoutParams(params); + + tabletTopBar.setSelectedTab(TabletTopBar.statusTabIndex); + + pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + super.onPageScrolled(position, positionOffset, positionOffsetPixels); + } + + @Override + public void onPageSelected(int position) { + super.onPageSelected(position); + + tabletTopBar.setSelectedTab(position); + } + + @Override + public void onPageScrollStateChanged(int state) { + super.onPageScrollStateChanged(state); + } + }); + + tabletTopBar.setClickWithIndexEventListener(this); + } + } + + @Override + public void onResume() { + super.onResume(); + + if (tabletTopBar.getVisibility() != View.GONE) { + tabletTopBar.onResume(); + } + } + + @Override + public void onPause() { + super.onPause(); + + if (tabletTopBar.getVisibility() != View.GONE) { + tabletTopBar.onPause(); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + tabLayoutMediator.detach(); + tabletTopBar.close(); + } + + @Override + public void onBackPressed() { + if (pager.getCurrentItem() != 0) { + pager.setCurrentItem(0); + } else { + super.onBackPressed(); + + if (VPNCoordinator.getInstance().isServiceRunning()) { + HelperFunctions.showToast(getString(R.string.general_service_running_notification), false); + } + } + } + + @Override + public void onOpenStatusRequested() { + pager.setCurrentItem(0); + } + + @Override + public void onOpenServerListRequested() { + pager.setCurrentItem(1); + } + + @Override + protected void onActivityResult(int request, int result, Intent data) { + super.onActivityResult(request, result, data); + + if (request == VPNCoordinator.VPN_PREPARATION_REQUEST_CODE) { + VPNCoordinator.getInstance().onActivityResult(request, result, data); + } + } + + @Override + public void onClickWithIndex(int index, Void data) { + pager.setCurrentItem(index); + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/index/IndexPageAdapter.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/index/IndexPageAdapter.java new file mode 100644 index 000000000..08905acac --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/index/IndexPageAdapter.java @@ -0,0 +1,49 @@ +package com.skywire.skycoin.vpn.activities.index; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; +import androidx.viewpager2.adapter.FragmentStateAdapter; + +import com.skywire.skycoin.vpn.activities.servers.ServersActivity; +import com.skywire.skycoin.vpn.activities.settings.SettingsActivity; +import com.skywire.skycoin.vpn.activities.start.StartActivity; + +public class IndexPageAdapter extends FragmentStateAdapter { + public interface RequestTabListener { + void onOpenStatusRequested(); + void onOpenServerListRequested(); + } + + private StartActivity tab1 = new StartActivity(); + private ServersActivity tab2 = new ServersActivity(); + private SettingsActivity tab3 = new SettingsActivity(); + + public IndexPageAdapter(AppCompatActivity activity) { + super(activity); + } + + public void setRequestTabListener(RequestTabListener listener) { + tab1.setRequestTabListener(listener); + tab2.setRequestTabListener(listener); + } + + @Override + public Fragment createFragment(int position) { + Fragment response; + + if (position == 0) { + response = tab1; + } else if (position == 1) { + response = tab2; + } else { + response = tab3; + } + + return response; + } + + @Override + public int getItemCount() { + return 3; + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/main/MainActivity.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/main/MainActivity.java new file mode 100644 index 000000000..222be391b --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/main/MainActivity.java @@ -0,0 +1,303 @@ +package com.skywire.skycoin.vpn.activities.main; + +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.activities.settings.SettingsActivity; +import com.skywire.skycoin.vpn.activities.start.StartActivity; +import com.skywire.skycoin.vpn.helpers.Notifications; +import com.skywire.skycoin.vpn.objects.LocalServerData; +import com.skywire.skycoin.vpn.objects.ManualVpnServerData; +import com.skywire.skycoin.vpn.vpn.VPNCoordinator; +import com.skywire.skycoin.vpn.vpn.VPNServersPersistentData; +import com.skywire.skycoin.vpn.vpn.VPNGeneralPersistentData; +import com.skywire.skycoin.vpn.vpn.VPNStates; +import com.skywire.skycoin.vpn.activities.apps.AppsActivity; +import com.skywire.skycoin.vpn.activities.servers.ServersActivity; +import com.skywire.skycoin.vpn.helpers.Globals; +import com.skywire.skycoin.vpn.helpers.HelperFunctions; + +import java.util.HashSet; + +import io.reactivex.rxjava3.disposables.Disposable; +import skywiremob.Skywiremob; + +public class MainActivity extends AppCompatActivity implements View.OnClickListener { + + private EditText editTextRemotePK; + private EditText editTextPasscode; + private Button buttonStart; + private Button buttonStop; + private Button buttonSelect; + private Button buttonApps; + private Button buttonSettings; + private Button buttonStartPage; + private TextView textLastError1; + private TextView textLastError2; + private TextView textStatus; + private TextView textFinishAlert; + private TextView textStopAlert; + + private Disposable serviceSubscription; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + editTextRemotePK = findViewById(R.id.editTextRemotePK); + editTextPasscode = findViewById(R.id.editTextPasscode); + buttonStart = findViewById(R.id.buttonStart); + buttonStop = findViewById(R.id.buttonStop); + buttonSelect = findViewById(R.id.buttonSelect); + buttonApps = findViewById(R.id.buttonApps); + buttonSettings = findViewById(R.id.buttonSettings); + buttonStartPage = findViewById(R.id.buttonStartPage); + textStatus = findViewById(R.id.textStatus); + textFinishAlert = findViewById(R.id.textFinishAlert); + textLastError1 = findViewById(R.id.textLastError1); + textLastError2 = findViewById(R.id.textLastError2); + textStopAlert = findViewById(R.id.textStopAlert); + + buttonStart.setOnClickListener(this); + buttonStop.setOnClickListener(this); + buttonSelect.setOnClickListener(this); + buttonApps.setOnClickListener(this); + buttonSettings.setOnClickListener(this); + buttonStartPage.setOnClickListener(this); + + LocalServerData currentServer = VPNServersPersistentData.getInstance().getCurrentServer(); + String savedPk = currentServer != null ? currentServer.pk : null; + String savedPassword = currentServer != null && currentServer.password != null ? currentServer.password : ""; + + if (savedPk != null && savedPassword != null) { + editTextRemotePK.setText(savedPk); + editTextPasscode.setText(savedPassword); + } + } + + @Override + public void onRestoreInstanceState(Bundle savedInstanceState) { + editTextRemotePK.setText(savedInstanceState.getString("pk")); + editTextPasscode.setText(savedInstanceState.getString("password")); + } + + @Override + public void onSaveInstanceState(@NonNull Bundle savedInstanceState) { + super.onSaveInstanceState(savedInstanceState); + savedInstanceState.putString("pk", editTextRemotePK.getText().toString()); + savedInstanceState.putString("password", editTextPasscode.getText().toString()); + } + + @Override + protected void onStart() { + super.onStart(); + + Notifications.removeAllAlertNotifications(); + + displayInitialState(); + + serviceSubscription = VPNCoordinator.getInstance().getEventsObservable().subscribe( + state -> { + if (state.state.val() < 10) { + displayInitialState(); + } else if (state.state != VPNStates.ERROR && state.state != VPNStates.BLOCKING_ERROR && state.state != VPNStates.DISCONNECTED) { + int stateText = VPNStates.getDescriptionForState(state.state); + + displayWorkingState(); + + if (state.startedByTheSystem) { + this.buttonStop.setEnabled(false); + textStopAlert.setVisibility(View.VISIBLE); + } + + if (state.stopRequested) { + this.buttonStop.setEnabled(false); + } + + if (stateText != -1) { + textStatus.setText(stateText); + } + } else if (state.state == VPNStates.DISCONNECTED) { + textStatus.setText(R.string.vpn_state_disconnected); + displayInitialState(); + } else { + textStatus.setText(VPNStates.getDescriptionForState(state.state)); + displayErrorState(state.stopRequested); + } + } + ); + } + + @Override + protected void onStop() { + super.onStop(); + + serviceSubscription.dispose(); + } + + @Override + public void onClick(View view) { + switch (view.getId()) { + case R.id.buttonStart: + start(); + break; + case R.id.buttonStop: + stop(); + break; + case R.id.buttonSelect: + selectServer(); + break; + case R.id.buttonApps: + selectApps(); + break; + case R.id.buttonSettings: + openSettings(); + break; + case R.id.buttonStartPage: + openStarPage(); + break; + } + } + + @Override + protected void onActivityResult(int request, int result, Intent data) { + super.onActivityResult(request, result, data); + + if (request == VPNCoordinator.VPN_PREPARATION_REQUEST_CODE) { + VPNCoordinator.getInstance().onActivityResult(request, result, data); + } else if (request == 1 && data != null) { + String address = data.getStringExtra(ServersActivity.ADDRESS_DATA_PARAM); + if (address != null) { + editTextRemotePK.setText(address); + editTextPasscode.setText(""); + } + + start(); + } + } + + private void start() { + // Check if the pk is valid. + String remotePK = editTextRemotePK.getText().toString().trim(); + long err = Skywiremob.isPKValid(remotePK).getCode(); + if (err != Skywiremob.ErrCodeNoError) { + HelperFunctions.showToast(getString(R.string.vpn_coordinator_invalid_credentials_error) + remotePK, false); + return; + } else { + Skywiremob.printString("PK is correct"); + } + + Globals.AppFilteringModes selectedMode = VPNGeneralPersistentData.getAppsSelectionMode(); + if (selectedMode != Globals.AppFilteringModes.PROTECT_ALL) { + HashSet selectedApps = HelperFunctions.filterAvailableApps(VPNGeneralPersistentData.getAppList(new HashSet<>())); + + if (selectedApps.size() == 0) { + if (selectedMode == Globals.AppFilteringModes.PROTECT_SELECTED) { + HelperFunctions.showToast(getString(R.string.vpn_no_apps_to_protect_warning), false); + } else { + HelperFunctions.showToast(getString(R.string.vpn_no_apps_to_ignore_warning), false); + } + } + } + + ManualVpnServerData intermediaryServerData = new ManualVpnServerData(); + intermediaryServerData.pk = remotePK; + intermediaryServerData.password = editTextPasscode.getText().toString(); + LocalServerData server = VPNServersPersistentData.getInstance().processFromManual(intermediaryServerData); + + VPNCoordinator.getInstance().startVPN( + this, + server + ); + } + + private void stop() { + VPNCoordinator.getInstance().stopVPN(); + } + + private void selectServer() { + Intent intent = new Intent(this, ServersActivity.class); + startActivityForResult(intent, 1); + } + + private void selectApps() { + Intent intent = new Intent(this, AppsActivity.class); + startActivity(intent); + } + + private void openSettings() { + Intent intent = new Intent(this, SettingsActivity.class); + startActivity(intent); + } + + private void openStarPage() { + Intent intent = new Intent(this, StartActivity.class); + startActivity(intent); + } + + private void displayInitialState() { + textStatus.setText(R.string.vpn_state_details_off); + + editTextRemotePK.setEnabled(true); + editTextPasscode.setEnabled(true); + buttonStart.setEnabled(true); + buttonStop.setEnabled(false); + buttonSelect.setEnabled(true); + buttonApps.setEnabled(true); + buttonSettings.setEnabled(true); + textFinishAlert.setVisibility(View.GONE); + textStopAlert.setVisibility(View.GONE); + + String lastError = VPNGeneralPersistentData.getLastError(null); + if (lastError != null) { + textLastError1.setVisibility(View.VISIBLE); + textLastError2.setVisibility(View.VISIBLE); + textLastError2.setText(lastError); + } else { + textLastError1.setVisibility(View.GONE); + textLastError2.setVisibility(View.GONE); + } + } + + private void displayWorkingState() { + editTextRemotePK.setEnabled(false); + editTextPasscode.setEnabled(false); + buttonStart.setEnabled(false); + buttonStop.setEnabled(true); + buttonSelect.setEnabled(false); + buttonApps.setEnabled(false); + buttonSettings.setEnabled(false); + textFinishAlert.setVisibility(View.GONE); + textStopAlert.setVisibility(View.GONE); + + textLastError1.setVisibility(View.GONE); + textLastError2.setVisibility(View.GONE); + } + + private void displayErrorState(boolean stopRequested) { + editTextRemotePK.setEnabled(false); + editTextPasscode.setEnabled(false); + buttonStart.setEnabled(false); + buttonStop.setEnabled(!stopRequested); + buttonSelect.setEnabled(false); + buttonApps.setEnabled(false); + buttonSettings.setEnabled(false); + textFinishAlert.setVisibility(stopRequested ? View.VISIBLE : View.GONE); + textStopAlert.setVisibility(View.GONE); + + textLastError1.setVisibility(View.VISIBLE); + textLastError2.setVisibility(View.VISIBLE); + + String lastError = VPNGeneralPersistentData.getLastError(null); + textLastError2.setText(lastError); + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/ConditionsList.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/ConditionsList.java new file mode 100644 index 000000000..987dbc6a1 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/ConditionsList.java @@ -0,0 +1,147 @@ +package com.skywire.skycoin.vpn.activities.servers; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.extensible.ButtonBase; +import com.skywire.skycoin.vpn.helpers.CountriesList; + +public class ConditionsList extends ButtonBase implements View.OnTouchListener { + private FrameLayout mainContainer; + private LinearLayout filtersContainer; + private LinearLayout orderContainer; + private TextView textFilters; + private TextView textOrder; + + public ConditionsList(Context context) { + super(context); + } + public ConditionsList(Context context, AttributeSet attrs) { + super(context, attrs); + } + public ConditionsList(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void Initialize (Context context, AttributeSet attrs) { + LayoutInflater inflater = (LayoutInflater)context.getSystemService (Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_server_list_condition_list, this, true); + + mainContainer = this.findViewById (R.id.mainContainer); + filtersContainer = this.findViewById (R.id.filtersContainer); + orderContainer = this.findViewById (R.id.orderContainer); + textFilters = this.findViewById (R.id.textFilters); + textOrder = this.findViewById (R.id.textOrder); + + mainContainer.setVisibility(GONE); + + setOnTouchListener(this); + setViewForCheckingClicks(this); + } + + public void setConditions(VpnServersAdapter.SortableColumns column, boolean sortingReversed, FilterModalWindow.Filters filters) { + if (filters == null && column == VpnServersAdapter.SortableColumns.AUTOMATIC) { + mainContainer.setVisibility(GONE); + } else { + boolean showingValues = false; + + if (filters != null) { + String filterList = ""; + if (filters.countryCode != null && !filters.countryCode.equals("")) { + filterList += getContext().getText(R.string.filter_server_country_label) + " \"" + CountriesList.getCountryName(filters.countryCode) + "\""; + } + + if (filters.name != null && !filters.name.equals("")) { + if (filterList.length() > 0) { + filterList += " / "; + } + + filterList += getContext().getText(R.string.filter_server_name_label) + " \"" + filters.name + "\""; + } + + if (filters.location != null && !filters.location.equals("")) { + if (filterList.length() > 0) { + filterList += " / "; + } + + filterList += getContext().getText(R.string.filter_server_location_label) + " \"" + filters.location + "\""; + } + + if (filters.pk != null && !filters.pk.equals("")) { + if (filterList.length() > 0) { + filterList += " / "; + } + + filterList += getContext().getText(R.string.filter_server_public_key_label) + " \"" + filters.pk + "\""; + } + + if (filters.note != null && !filters.note.equals("")) { + if (filterList.length() > 0) { + filterList += " / "; + } + + filterList += getContext().getText(R.string.filter_server_note_label) + " \"" + filters.note + "\""; + } + + if (filterList.length() > 0) { + filtersContainer.setVisibility(VISIBLE); + textFilters.setText(filterList); + + showingValues = true; + } else { + filtersContainer.setVisibility(GONE); + } + } else { + filtersContainer.setVisibility(GONE); + } + + if (column != VpnServersAdapter.SortableColumns.AUTOMATIC) { + String columnName = getContext().getText(VpnServersAdapter.SortableColumns.getColumnNameId(column)).toString(); + + if (sortingReversed) { + columnName += " " + getContext().getText(R.string.tmp_select_server_reversed_suffix); + } + + orderContainer.setVisibility(VISIBLE); + textOrder.setText(getContext().getText(R.string.tmp_select_server_sorted_by_prefix) + " \"" + columnName + "\""); + + showingValues = true; + } else { + orderContainer.setVisibility(GONE); + } + + if (showingValues) { + mainContainer.setVisibility(VISIBLE); + } else { + mainContainer.setVisibility(GONE); + } + } + } + + public boolean showingFilters() { + return mainContainer.getVisibility() != GONE && filtersContainer.getVisibility() != GONE; + } + + public boolean showingOrder() { + return mainContainer.getVisibility() != GONE && orderContainer.getVisibility() != GONE; + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + setAlpha(0.5f); + } else if (event.getAction() == MotionEvent.ACTION_CANCEL || event.getAction() == MotionEvent.ACTION_POINTER_UP || event.getAction() == MotionEvent.ACTION_UP) { + setAlpha(1f); + } + + return false; + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/FilterModalWindow.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/FilterModalWindow.java new file mode 100644 index 000000000..cd0811891 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/FilterModalWindow.java @@ -0,0 +1,167 @@ +package com.skywire.skycoin.vpn.activities.servers; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.View; +import android.view.Window; +import android.view.inputmethod.EditorInfo; +import android.widget.EditText; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.controls.ModalWindowButton; +import com.skywire.skycoin.vpn.controls.Select; +import com.skywire.skycoin.vpn.extensible.ClickEvent; +import com.skywire.skycoin.vpn.helpers.CountriesList; +import com.skywire.skycoin.vpn.helpers.HelperFunctions; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; + +public class FilterModalWindow extends Dialog implements ClickEvent { + public static class Filters { + public String countryCode; + public String name; + public String location; + public String pk; + public String note; + } + + public interface Confirmed { + void confirmed(Filters filters); + } + + private Select selectCountry; + private EditText editName; + private EditText editLocation; + private EditText editPk; + private EditText editNote; + private ModalWindowButton buttonCancel; + private ModalWindowButton buttonConfirm; + + private HashSet countries; + private Filters currentFilters; + private Confirmed event; + + public FilterModalWindow(Context ctx, HashSet countries, Filters currentFilters, Confirmed event) { + super(ctx); + + this.countries = countries; + this.currentFilters = currentFilters; + this.event = event; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_NO_TITLE); + setContentView(R.layout.view_server_filters_modal); + + selectCountry = findViewById(R.id.selectCountry); + editName = findViewById(R.id.editName); + editLocation = findViewById(R.id.editLocation); + editPk = findViewById(R.id.editPk); + editNote = findViewById(R.id.editNote); + buttonCancel = findViewById(R.id.buttonCancel); + buttonConfirm = findViewById(R.id.buttonConfirm); + + ArrayList countryOptions = new ArrayList<>(); + Select.SelectOption option = new Select.SelectOption(); + option.text = getContext().getString(R.string.filter_server_any_country_option); + countryOptions.add(option); + + Comparator comparator = (a, b) -> a.compareTo(b); + ArrayList countriesList = new ArrayList<>(countries); + Collections.sort(countriesList, comparator); + + int i = 1; + HashMap countryIndexMap = new HashMap<>(); + for (String countryCode : countriesList) { + countryCode = countryCode.toLowerCase(); + option = new Select.SelectOption(); + option.text = CountriesList.getCountryName(countryCode); + option.value = countryCode; + option.iconId = HelperFunctions.getFlagResourceId(countryCode); + countryOptions.add(option); + + countryIndexMap.put(countryCode, i); + i++; + } + + if (currentFilters != null) { + editName.setText(currentFilters.name); + editLocation.setText(currentFilters.location); + editPk.setText(currentFilters.pk); + editNote.setText(currentFilters.note); + } + + editName.setSelection(editName.getText().length()); + + if (currentFilters != null && currentFilters.countryCode != null) { + int selectedIndex = countryIndexMap.containsKey(currentFilters.countryCode) ? countryIndexMap.get(currentFilters.countryCode) : 0; + selectCountry.setValues(countryOptions, selectedIndex); + } else { + selectCountry.setValues(countryOptions, 0); + } + + editName.setImeOptions(EditorInfo.IME_ACTION_NEXT); + editLocation.setImeOptions(EditorInfo.IME_ACTION_NEXT); + editPk.setImeOptions(EditorInfo.IME_ACTION_NEXT); + editNote.setImeOptions(EditorInfo.IME_ACTION_DONE); + + editNote.setOnEditorActionListener((v, actionId, event) -> { + if ( + actionId == EditorInfo.IME_ACTION_DONE || + (event != null && event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_ENTER) + ) { + process(); + dismiss(); + + return true; + } + + return false; + }); + + buttonCancel.setClickEventListener(this); + buttonConfirm.setClickEventListener(this); + + HelperFunctions.configureModalWindow(this); + } + + @Override + public void onClick(View view) { + if (view.getId() == R.id.buttonConfirm) { + process(); + } + + dismiss(); + } + + private void process() { + if (event != null) { + Filters filters = new Filters(); + + filters.countryCode = selectCountry.getSelectedValue(); + + if (editName.getText() != null && !editName.getText().toString().trim().equals("")) { + filters.name = editName.getText().toString().trim(); + } + if (editLocation.getText() != null && !editLocation.getText().toString().trim().equals("")) { + filters.location = editLocation.getText().toString().trim(); + } + if (editPk.getText() != null && !editPk.getText().toString().trim().equals("")) { + filters.pk = editPk.getText().toString().trim(); + } + if (editNote.getText() != null && !editNote.getText().toString().trim().equals("")) { + filters.note = editNote.getText().toString().trim(); + } + + event.confirmed(filters); + } + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/ServerListButton.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/ServerListButton.java new file mode 100644 index 000000000..fc2d4cfab --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/ServerListButton.java @@ -0,0 +1,176 @@ +package com.skywire.skycoin.vpn.activities.servers; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.controls.BoxRowLayout; +import com.skywire.skycoin.vpn.controls.ServerName; +import com.skywire.skycoin.vpn.controls.SettingsButton; +import com.skywire.skycoin.vpn.extensible.ListButtonBase; +import com.skywire.skycoin.vpn.helpers.BoxRowTypes; +import com.skywire.skycoin.vpn.helpers.HelperFunctions; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; + +public class ServerListButton extends ListButtonBase { + public static final float APROX_HEIGHT_DP = 55; + + private static DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd hh:mm a"); + + private BoxRowLayout mainLayout; + private ImageView imageFlag; + private ServerName serverName; + private TextView textDate; + private TextView textLocation; + private TextView textNote; + private TextView textPersonalNote; + private LinearLayout noteArea; + private LinearLayout personalNoteArea; + private SettingsButton buttonSettings; + + // TODO: for currently commented fields, must be deleted or reactivated depending on what happens to the fields. + /* + private LinearLayout statsArea1; + private LinearLayout statsArea2; + private TextView textLatency; + private TextView textCongestion; + private TextView textHops; + private TextView textLatencyRating; + private TextView textCongestionRating; + */ + + private VpnServerForList server; + private ServerLists listType; + + public ServerListButton (Context context) { + super(context); + } + + @Override + protected void Initialize (Context context, AttributeSet attrs) { + LayoutInflater inflater = (LayoutInflater)context.getSystemService (Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_server_list_item, this, true); + + mainLayout = this.findViewById (R.id.mainLayout); + imageFlag = this.findViewById (R.id.imageFlag); + serverName = this.findViewById (R.id.serverName); + textDate = this.findViewById (R.id.textDate); + textLocation = this.findViewById (R.id.textLocation); + textNote = this.findViewById (R.id.textNote); + textPersonalNote = this.findViewById (R.id.textPersonalNote); + noteArea = this.findViewById (R.id.noteArea); + personalNoteArea = this.findViewById (R.id.personalNoteArea); + buttonSettings = this.findViewById (R.id.buttonSettings); + + // TODO: for currently commented fields, must be deleted or reactivated depending on what happens to the fields. + /* + statsArea1 = this.findViewById (R.id.statsArea1); + statsArea2 = this.findViewById (R.id.statsArea2); + textLatency = this.findViewById (R.id.textLatency); + textCongestion = this.findViewById (R.id.textCongestion); + textHops = this.findViewById (R.id.textHops); + textLatencyRating = this.findViewById (R.id.textLatencyRating); + textCongestionRating = this.findViewById (R.id.textCongestionRating); + */ + + imageFlag.setClipToOutline(true); + + buttonSettings.setClickEventListener(view -> showOptions()); + + setClickableBoxView(mainLayout); + } + + public void changeData(@NonNull VpnServerForList serverData, ServerLists listType) { + server = serverData; + this.listType = listType; + + imageFlag.setImageResource(HelperFunctions.getFlagResourceId(serverData.countryCode)); + serverName.setServer(serverData, listType, false); + + if (serverData.location != null && !serverData.location.trim().equals("")) { + String pk = serverData.pk; + if (pk.length() > 5) { + pk = pk.substring(0, 5); + } + textLocation.setText("(" + pk + ") " + serverData.location); + } else { + textLocation.setText(serverData.pk); + } + + if (serverData.note != null && serverData.note.trim() != "") { + noteArea.setVisibility(VISIBLE); + textNote.setText(serverData.note); + } else { + noteArea.setVisibility(GONE); + } + if (serverData.personalNote != null && serverData.personalNote.trim() != "") { + personalNoteArea.setVisibility(VISIBLE); + textPersonalNote.setText(serverData.personalNote); + } else { + personalNoteArea.setVisibility(GONE); + } + + // TODO: for currently commented fields, must be deleted or reactivated depending on what happens to the fields. + /* + if (listType == ServerLists.Public) { + statsArea1.setVisibility(VISIBLE); + statsArea2.setVisibility(VISIBLE); + + textLatency.setText(HelperFunctions.getLatencyValue(serverData.latency)); + textCongestion.setText(HelperFunctions.zeroDecimalsFormatter.format(serverData.congestion) + "%"); + textHops.setText(serverData.hops + ""); + + textLatencyRating.setText(ServerRatings.getTextForRating(serverData.latencyRating)); + textLatencyRating.setTextColor(getRatingColor(serverData.latencyRating)); + textCongestionRating.setText(ServerRatings.getTextForRating(serverData.congestionRating)); + textCongestionRating.setTextColor(getRatingColor(serverData.congestionRating)); + + textCongestion.setTextColor(HelperFunctions.getCongestionNumberColor((int)serverData.congestion)); + textLatency.setTextColor(HelperFunctions.getLatencyNumberColor((int)serverData.latency)); + textHops.setTextColor(HelperFunctions.getHopsNumberColor((int)serverData.hops)); + } else { + statsArea1.setVisibility(GONE); + statsArea2.setVisibility(GONE); + } + */ + + if (listType == ServerLists.History) { + textDate.setVisibility(VISIBLE); + textDate.setText(dateFormat.format(serverData.lastUsed)); + } else { + textDate.setVisibility(GONE); + } + } + + public void setBoxRowType(BoxRowTypes type) { + mainLayout.setType(type); + } + + // TODO: for currently commented fields, must be deleted or reactivated depending on what happens to the fields. + /* + private int getRatingColor(ServerRatings rating) { + int colorId = R.color.bronze; + + if (rating == ServerRatings.Gold) { + colorId = R.color.gold; + } else if (rating == ServerRatings.Silver) { + colorId = R.color.silver; + } + + return ContextCompat.getColor(getContext(), colorId); + } + */ + + private void showOptions() { + HelperFunctions.showServerOptions(getContext(), server, listType); + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/ServerListOptionButton.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/ServerListOptionButton.java new file mode 100644 index 000000000..be47576a0 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/ServerListOptionButton.java @@ -0,0 +1,53 @@ +package com.skywire.skycoin.vpn.activities.servers; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.widget.TextView; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.controls.BoxRowLayout; +import com.skywire.skycoin.vpn.extensible.ButtonBase; + +public class ServerListOptionButton extends ButtonBase { + + private BoxRowLayout mainLayout; + private TextView textIcon; + + public ServerListOptionButton(Context context) { + super(context); + } + public ServerListOptionButton(Context context, AttributeSet attrs) { + super(context, attrs); + } + public ServerListOptionButton(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void Initialize (Context context, AttributeSet attrs) { + LayoutInflater inflater = (LayoutInflater)context.getSystemService (Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_server_list_option_button, this, true); + + mainLayout = this.findViewById (R.id.mainLayout); + textIcon = this.findViewById (R.id.textIcon); + + if (attrs != null) { + TypedArray attributes = context.getTheme().obtainStyledAttributes( + attrs, + R.styleable.ServerListOptionButton, + 0, 0 + ); + + String content = attributes.getString(R.styleable.ServerListOptionButton_content); + if (content != null && content.trim() != "") { + textIcon.setText(content); + } + + attributes.recycle(); + } + + setClickableBoxView(mainLayout); + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/ServerListOptions.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/ServerListOptions.java new file mode 100644 index 000000000..89dbfa432 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/ServerListOptions.java @@ -0,0 +1,121 @@ +package com.skywire.skycoin.vpn.activities.servers; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import androidx.recyclerview.widget.RecyclerView; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.controls.BoxRowLayout; +import com.skywire.skycoin.vpn.extensible.ClickEvent; +import com.skywire.skycoin.vpn.extensible.ClickWithIndexEvent; +import com.skywire.skycoin.vpn.helpers.HelperFunctions; + +public class ServerListOptions extends FrameLayout implements ClickEvent { + public static final int filterIndex = -1; + public static final int addIndex = -2; + public static final int sortIndex = -3; + public static final int showPublicIndex = -10; + public static final int showHistoryIndex = -11; + public static final int showFavoritesIndex = -12; + public static final int showBlockedIndex = -13; + + public ServerListOptions(Context context) { + super(context); + Initialize(context, null); + } + public ServerListOptions(Context context, AttributeSet attrs) { + super(context, attrs); + Initialize(context, attrs); + } + public ServerListOptions(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + Initialize(context, attrs); + } + + private BoxRowLayout tabsContainer; + private ServerListTopTab tabPublic; + private ServerListTopTab tabHistory; + private ServerListTopTab tabFavorites; + private ServerListTopTab tabBlocked; + private ServerListOptionButton buttonSort; + private ServerListOptionButton buttonFilter; + private ServerListOptionButton buttonAdd; + + private ClickWithIndexEvent clickListener; + + protected void Initialize (Context context, AttributeSet attrs) { + LayoutInflater inflater = (LayoutInflater)context.getSystemService (Context.LAYOUT_INFLATER_SERVICE); + View rootView = inflater.inflate(R.layout.view_server_list_options, this, true); + + tabsContainer = this.findViewById (R.id.tabsContainer); + tabPublic = this.findViewById (R.id.tabPublic); + tabHistory = this.findViewById (R.id.tabHistory); + tabFavorites = this.findViewById (R.id.tabFavorites); + tabBlocked = this.findViewById (R.id.tabBlocked); + buttonSort = this.findViewById (R.id.buttonSort); + buttonFilter = this.findViewById (R.id.buttonFilter); + buttonAdd = this.findViewById (R.id.buttonAdd); + + tabPublic.setClickEventListener(this); + tabHistory.setClickEventListener(this); + tabFavorites.setClickEventListener(this); + tabBlocked.setClickEventListener(this); + buttonSort.setClickEventListener(this); + buttonFilter.setClickEventListener(this); + buttonAdd.setClickEventListener(this); + + RecyclerView.LayoutParams params = new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + rootView.setLayoutParams(params); + + if (HelperFunctions.getWidthType(getContext()) == HelperFunctions.WidthTypes.SMALL) { + tabsContainer.setVisibility(GONE); + } + } + + public void setClickWithIndexEventListener(ClickWithIndexEvent listener) { + clickListener = listener; + } + + public void selectCorrectTab(ServerLists currentListType) { + tabPublic.changeState(false); + tabHistory.changeState(false); + tabFavorites.changeState(false); + tabBlocked.changeState(false); + + if (currentListType == ServerLists.Public) { + tabPublic.changeState(true); + } else if (currentListType == ServerLists.History) { + tabHistory.changeState(true); + } else if (currentListType == ServerLists.Favorites) { + tabFavorites.changeState(true); + } else if (currentListType == ServerLists.Blocked) { + tabBlocked.changeState(true); + } + } + + @Override + public void onClick(View view) { + if (clickListener != null) { + if (view.getId() == R.id.tabPublic) { + clickListener.onClickWithIndex(showPublicIndex, null); + } else if (view.getId() == R.id.tabHistory) { + clickListener.onClickWithIndex(showHistoryIndex, null); + } else if (view.getId() == R.id.tabFavorites) { + clickListener.onClickWithIndex(showFavoritesIndex, null); + } else if (view.getId() == R.id.tabBlocked) { + clickListener.onClickWithIndex(showBlockedIndex, null); + } else if (view.getId() == R.id.buttonSort) { + clickListener.onClickWithIndex(sortIndex, null); + } else if (view.getId() == R.id.buttonAdd) { + clickListener.onClickWithIndex(addIndex, null); + } else if (view.getId() == R.id.buttonFilter) { + clickListener.onClickWithIndex(filterIndex, null); + } + } + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/ServerListTableHeader.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/ServerListTableHeader.java new file mode 100644 index 000000000..ef33a7c91 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/ServerListTableHeader.java @@ -0,0 +1,45 @@ +package com.skywire.skycoin.vpn.activities.servers; + +import android.content.Context; +import android.view.LayoutInflater; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.skywire.skycoin.vpn.R; + +public class ServerListTableHeader extends FrameLayout { + private TextView textDate; + + // TODO: for currently commented fields, must be deleted or reactivated depending on what happens to the fields. + // private LinearLayout statsArea; + + public ServerListTableHeader(Context context) { + super(context); + + LayoutInflater inflater = (LayoutInflater)context.getSystemService (Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_server_list_table_header, this, true); + + textDate = this.findViewById (R.id.textDate); + + // TODO: for currently commented fields, must be deleted or reactivated depending on what happens to the fields. + // statsArea = this.findViewById (R.id.statsArea); + } + + public void setListType(ServerLists listType) { + // TODO: for currently commented fields, must be deleted or reactivated depending on what happens to the fields. + /* + if (listType == ServerLists.Public) { + statsArea.setVisibility(VISIBLE); + } else { + statsArea.setVisibility(GONE); + } + */ + + if (listType == ServerLists.History) { + textDate.setVisibility(VISIBLE); + } else { + textDate.setVisibility(GONE); + } + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/ServerListTableRow.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/ServerListTableRow.java new file mode 100644 index 000000000..707081cd6 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/ServerListTableRow.java @@ -0,0 +1,162 @@ +package com.skywire.skycoin.vpn.activities.servers; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.controls.BoxRowLayout; +import com.skywire.skycoin.vpn.controls.ServerName; +import com.skywire.skycoin.vpn.controls.ServerNotesModalWindow; +import com.skywire.skycoin.vpn.controls.SettingsButton; +import com.skywire.skycoin.vpn.extensible.ListButtonBase; +import com.skywire.skycoin.vpn.helpers.BoxRowTypes; +import com.skywire.skycoin.vpn.helpers.HelperFunctions; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; + +public class ServerListTableRow extends ListButtonBase { + public static final float APROX_HEIGHT_DP = 50; + + private static DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd hh:mm a"); + + private BoxRowLayout mainLayout; + private ImageView imageFlag; + private ServerName serverName; + private TextView textDate; + private TextView textLocation; + private TextView textPk; + private SettingsButton buttonNote; + private SettingsButton buttonSettings; + + // TODO: for currently commented fields, must be deleted or reactivated depending on what happens to the fields. + /* + private ImageView imageCongestionRating; + private ImageView imageLatencyRating; + private TextView textCongestion; + private TextView textLatency; + private TextView textHops; + private LinearLayout statsArea; + */ + + private VpnServerForList server; + private ServerLists listType; + + public ServerListTableRow(Context context) { + super(context); + } + + @Override + protected void Initialize (Context context, AttributeSet attrs) { + LayoutInflater inflater = (LayoutInflater)context.getSystemService (Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_server_list_table_row, this, true); + + mainLayout = this.findViewById (R.id.mainLayout); + imageFlag = this.findViewById (R.id.imageFlag); + serverName = this.findViewById (R.id.serverName); + textDate = this.findViewById (R.id.textDate); + textLocation = this.findViewById (R.id.textLocation); + textPk = this.findViewById (R.id.textPk); + buttonNote = this.findViewById (R.id.buttonNote); + buttonSettings = this.findViewById (R.id.buttonSettings); + + // TODO: for currently commented fields, must be deleted or reactivated depending on what happens to the fields. + /* + imageCongestionRating = this.findViewById (R.id.imageCongestionRating); + imageLatencyRating = this.findViewById (R.id.imageLatencyRating); + textCongestion = this.findViewById (R.id.textCongestion); + textLatency = this.findViewById (R.id.textLatency); + textHops = this.findViewById (R.id.textHops); + statsArea = this.findViewById (R.id.statsArea); + */ + + imageFlag.setClipToOutline(true); + + buttonNote.setClickEventListener(view -> showNotes()); + buttonSettings.setClickEventListener(view -> showOptions()); + + setClickableBoxView(mainLayout); + } + + public void changeData(@NonNull VpnServerForList serverData, ServerLists listType) { + server = serverData; + this.listType = listType; + + imageFlag.setImageResource(HelperFunctions.getFlagResourceId(serverData.countryCode)); + serverName.setServer(serverData, listType, false); + + if (serverData.location != null && serverData.location.trim().length() > 0) { + textLocation.setText(serverData.location); + } else { + textLocation.setText(R.string.tmp_select_server_unknown_location); + } + + textPk.setText(serverData.pk); + + if ((serverData.note == null || serverData.note.equals("")) && (serverData.personalNote == null || serverData.personalNote.equals(""))) { + buttonNote.setVisibility(GONE); + } else { + buttonNote.setVisibility(VISIBLE); + } + + // TODO: for currently commented fields, must be deleted or reactivated depending on what happens to the fields. + /* + if (listType == ServerLists.Public) { + statsArea.setVisibility(VISIBLE); + + textCongestion.setText(HelperFunctions.zeroDecimalsFormatter.format(serverData.congestion) + "%"); + textLatency.setText(HelperFunctions.getLatencyValue(serverData.latency)); + textHops.setText(serverData.hops + ""); + + textCongestion.setTextColor(HelperFunctions.getCongestionNumberColor((int)serverData.congestion)); + textLatency.setTextColor(HelperFunctions.getLatencyNumberColor((int)serverData.latency)); + textHops.setTextColor(HelperFunctions.getHopsNumberColor((int)serverData.hops)); + + imageCongestionRating.setImageResource(getRatingResource(serverData.congestionRating)); + imageLatencyRating.setImageResource(getRatingResource(serverData.latencyRating)); + } else { + statsArea.setVisibility(GONE); + } + */ + + if (listType == ServerLists.History) { + textDate.setVisibility(VISIBLE); + textDate.setText(dateFormat.format(serverData.lastUsed)); + } else { + textDate.setVisibility(GONE); + } + } + + public void setBoxRowType(BoxRowTypes type) { + mainLayout.setType(type); + } + + // TODO: for currently commented fields, must be deleted or reactivated depending on what happens to the fields. + // TODO: if the fields are removed, the images should be removed too. + /* + private int getRatingResource(ServerRatings rating) { + if (rating == ServerRatings.Gold) { + return R.drawable.gold_rating; + } else if (rating == ServerRatings.Silver) { + return R.drawable.silver_rating; + } + + return R.drawable.bronze_rating; + } + */ + + private void showNotes() { + ServerNotesModalWindow modal = new ServerNotesModalWindow(getContext(), server); + modal.show(); + } + + private void showOptions() { + HelperFunctions.showServerOptions(getContext(), server, listType); + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/ServerListTopTab.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/ServerListTopTab.java new file mode 100644 index 000000000..8be2b43c7 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/ServerListTopTab.java @@ -0,0 +1,94 @@ +package com.skywire.skycoin.vpn.activities.servers; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.RippleDrawable; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.TextView; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.extensible.ButtonBase; + +public class ServerListTopTab extends ButtonBase implements View.OnTouchListener { + private FrameLayout mainLayout; + private View clickBackground; + private TextView text; + + private RippleDrawable rippleDrawable; + + public ServerListTopTab(Context context) { + super(context); + } + public ServerListTopTab(Context context, AttributeSet attrs) { + super(context, attrs); + } + public ServerListTopTab(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void Initialize (Context context, AttributeSet attrs) { + LayoutInflater inflater = (LayoutInflater)context.getSystemService (Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_server_list_top_tab, this, true); + + mainLayout = this.findViewById (R.id.mainLayout); + clickBackground = this.findViewById (R.id.clickBackground); + text = this.findViewById (R.id.text); + + rippleDrawable = (RippleDrawable) clickBackground.getBackground(); + + if (attrs != null) { + TypedArray attributes = context.getTheme().obtainStyledAttributes( + attrs, + R.styleable.ServerListTopTab, + 0, 0 + ); + + int corner = attributes.getInteger(R.styleable.ServerListTopTab_position, 0); + if (corner != 0) { + if (corner == 1) { + mainLayout.setBackgroundResource(R.drawable.box_clip_area_left); + } else if (corner == 2) { + mainLayout.setBackgroundResource(R.drawable.box_clip_area_right); + } + + mainLayout.setClipToOutline(true); + } + + String txt = attributes.getString(R.styleable.ServerListTopTab_text); + if (txt != null && !txt.trim().equals("")) { + text.setText(txt); + } + + attributes.recycle(); + } + + clickBackground.setOnTouchListener(this); + setViewForCheckingClicks(clickBackground); + } + + public void changeState(boolean selected) { + if (selected) { + clickBackground.setBackgroundResource(R.color.tablet_selected_tab_background); + rippleDrawable = null; + this.setClickable(false); + } else { + clickBackground.setBackgroundResource(R.drawable.box_ripple); + rippleDrawable = (RippleDrawable) clickBackground.getBackground(); + this.setClickable(true); + } + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (rippleDrawable != null) { + rippleDrawable.setHotspot(event.getX(), event.getY()); + } + + return false; + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/ServerLists.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/ServerLists.java new file mode 100644 index 000000000..48c6b2b80 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/ServerLists.java @@ -0,0 +1,8 @@ +package com.skywire.skycoin.vpn.activities.servers; + +public enum ServerLists { + Public, + History, + Favorites, + Blocked +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/ServersActivity.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/ServersActivity.java new file mode 100644 index 000000000..df46ce036 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/ServersActivity.java @@ -0,0 +1,437 @@ +package com.skywire.skycoin.vpn.activities.servers; + +import android.content.SharedPreferences; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.preference.PreferenceManager; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.gson.Gson; +import com.skywire.skycoin.vpn.App; +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.activities.index.IndexPageAdapter; +import com.skywire.skycoin.vpn.controls.Tab; +import com.skywire.skycoin.vpn.extensible.ClickEvent; +import com.skywire.skycoin.vpn.helpers.HelperFunctions; +import com.skywire.skycoin.vpn.network.ApiClient; +import com.skywire.skycoin.vpn.objects.LocalServerData; +import com.skywire.skycoin.vpn.objects.ServerFlags; +import com.skywire.skycoin.vpn.vpn.VPNCoordinator; +import com.skywire.skycoin.vpn.vpn.VPNServersPersistentData; + +import java.util.ArrayList; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public class ServersActivity extends Fragment implements VpnServersAdapter.VpnServerListEventListener, ClickEvent { + public static String ADDRESS_DATA_PARAM = "address"; + private static final String ACTIVE_TAB_KEY = "activeTab"; + + private Tab tabPublic; + private Tab tabHistory; + private Tab tabFavorites; + private Tab tabBlocked; + private RecyclerView recycler; + private ProgressBar loadingAnimation; + private TextView textNoResults; + private LinearLayout noResultsContainer; + private LinearLayout bottomTabsContainer; + private FrameLayout internalContainer; + private ImageView ImageBottomTabsShadow; + + private IndexPageAdapter.RequestTabListener requestTabListener; + private ServerLists listType = ServerLists.Public; + private VpnServersAdapter adapter; + private SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(App.getContext()); + + private Disposable serverSubscription; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); + + return inflater.inflate(R.layout.activity_server_list, container, true); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + tabPublic = view.findViewById(R.id.tabPublic); + tabHistory = view.findViewById(R.id.tabHistory); + tabFavorites = view.findViewById(R.id.tabFavorites); + tabBlocked = view.findViewById(R.id.tabBlocked); + recycler = view.findViewById(R.id.recycler); + loadingAnimation = view.findViewById(R.id.loadingAnimation); + textNoResults = view.findViewById(R.id.textNoResults); + noResultsContainer = view.findViewById(R.id.noResultsContainer); + bottomTabsContainer = view.findViewById(R.id.bottomTabsContainer); + internalContainer = view.findViewById(R.id.internalContainer); + ImageBottomTabsShadow = view.findViewById(R.id.ImageBottomTabsShadow); + + tabPublic.setClickEventListener(this); + tabHistory.setClickEventListener(this); + tabFavorites.setClickEventListener(this); + tabBlocked.setClickEventListener(this); + + LinearLayoutManager layoutManager = new LinearLayoutManager(getContext()); + recycler.setLayoutManager(layoutManager); + + // This code retrieves the data from the server and populates the list with the recovered + // data, but is not used right now as the server is returning empty arrays. + // requestData() + + noResultsContainer.setVisibility(View.GONE); + loadingAnimation.setVisibility(View.VISIBLE); + + // Initialize the recycler. + adapter = new VpnServersAdapter(getContext()); + adapter.setVpnServerListEventListener(this); + adapter.setData(new ArrayList<>(), listType); + recycler.setAdapter(adapter); + + Gson gson = new Gson(); + String savedlistType = settings.getString(ACTIVE_TAB_KEY, null); + if (savedlistType != null) { + listType = gson.fromJson(savedlistType, ServerLists.class); + } + + showCorrectList(); + + if (HelperFunctions.getWidthType(getContext()) != HelperFunctions.WidthTypes.SMALL) { + bottomTabsContainer.setVisibility(View.GONE); + ImageBottomTabsShadow.setVisibility(View.GONE); + + FrameLayout.LayoutParams params = (FrameLayout.LayoutParams)internalContainer.getLayoutParams(); + params.bottomMargin = 0; + internalContainer.setLayoutParams(params); + } + } + + public void setRequestTabListener(IndexPageAdapter.RequestTabListener listener) { + requestTabListener = listener; + } + + @Override + public void tabChangeRequested(ServerLists newListType) { + if (newListType != listType) { + listType = newListType; + + finishChangingTab(); + } + } + + @Override + public void onClick(View view) { + if (view.getId() == R.id.tabPublic) { + listType = ServerLists.Public; + } else if (view.getId() == R.id.tabHistory) { + listType = ServerLists.History; + } else if (view.getId() == R.id.tabFavorites) { + listType = ServerLists.Favorites; + } else if (view.getId() == R.id.tabBlocked) { + listType = ServerLists.Blocked; + } + + finishChangingTab(); + } + + private void finishChangingTab() { + Gson gson = new Gson(); + String listTypeString = gson.toJson(listType); + settings.edit() + .putString(ACTIVE_TAB_KEY, listTypeString) + .apply(); + + showCorrectList(); + } + + private void showCorrectList() { + tabPublic.changeState(false); + tabHistory.changeState(false); + tabFavorites.changeState(false); + tabBlocked.changeState(false); + + if (listType == ServerLists.Public) { + tabPublic.changeState(true); + // Use test data, for now. + showTestServers(); + } else { + if (listType == ServerLists.History) { + tabHistory.changeState(true); + } else if (listType == ServerLists.Favorites) { + tabFavorites.changeState(true); + } else if (listType == ServerLists.Blocked) { + tabBlocked.changeState(true); + } + + requestLocalData(); + } + } + + private void requestData() { + if (serverSubscription != null) { + serverSubscription.dispose(); + } +/* + serverSubscription = ApiClient.getVpnServers().subscribe(response -> { + ArrayList list = new ArrayList<>(); + + for (LocalServerData server : response) { + list.add(convertLocalServerData(server)); + } + + + VpnServersAdapter adapter = new VpnServersAdapter(this, response.body()); + adapter.setVpnSelectedEventListener(this); + recycler.setAdapter(adapter); + + // TODO: addSavedData will remove all blocked servers, so it will have to be called + // every time the blocked servers list changes. + }, err -> { + this.requestData(); + }); + */ + } + + private void requestLocalData() { + if (serverSubscription != null) { + serverSubscription.dispose(); + } + + adapter.setData(new ArrayList<>(), listType); + noResultsContainer.setVisibility(View.GONE); + loadingAnimation.setVisibility(View.VISIBLE); + + Observable> request; + if (listType == ServerLists.History) { + request = VPNServersPersistentData.getInstance().history(); + } else if (listType == ServerLists.Favorites) { + request = VPNServersPersistentData.getInstance().favorites(); + } else { + request = VPNServersPersistentData.getInstance().blocked(); + } + + serverSubscription = request.subscribe(response -> { + ArrayList list = new ArrayList<>(); + + for (LocalServerData server : response) { + list.add(convertLocalServerData(server)); + } + + loadingAnimation.setVisibility(View.GONE); + + adapter.setData(list, listType); + }); + } + + public static VpnServerForList convertLocalServerData(LocalServerData server) { + if (server == null) { + return null; + } + + VpnServerForList converted = new VpnServerForList(); + + converted.countryCode = server.countryCode; + converted.name = server.name; + converted.customName = server.customName; + converted.location = server.location; + converted.pk = server.pk; + converted.note = server.note; + converted.personalNote = server.personalNote; + converted.lastUsed = server.lastUsed; + converted.inHistory = server.inHistory; + converted.flag = server.flag; + converted.enteredManually = server.enteredManually; + converted.hasPassword = server.password != null && !server.password.equals(""); + + return converted; + } + + @Override + public void onResume() { + super.onResume(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + + if (serverSubscription != null) { + serverSubscription.dispose(); + } + } + + @Override + public void onVpnServerSelected(VpnServerForList selectedServer) { + start(VPNServersPersistentData.getInstance().processFromList(selectedServer)); + } + + @Override + public void onManualEntered(LocalServerData server) { + start(server); + } + + @Override + public void listHasElements(boolean hasElements, boolean emptyBecauseFilters) { + if (hasElements || loadingAnimation.getVisibility() != View.GONE) { + noResultsContainer.setVisibility(View.GONE); + } else { + noResultsContainer.setVisibility(View.VISIBLE); + + if (emptyBecauseFilters) { + textNoResults.setText(R.string.tmp_select_server_empty_with_filter); + } else { + if (listType == ServerLists.History) { + textNoResults.setText(R.string.tmp_select_server_empty_history); + } else if (listType == ServerLists.Favorites) { + textNoResults.setText(R.string.tmp_select_server_empty_favorites); + } else if (listType == ServerLists.Blocked) { + textNoResults.setText(R.string.tmp_select_server_empty_blocked); + } else { + textNoResults.setText(R.string.tmp_select_server_empty_discovery); + } + } + } + } + + private void start(LocalServerData server) { + if (VPNCoordinator.getInstance().isServiceRunning()) { + HelperFunctions.showToast(getContext().getText(R.string.tmp_select_server_running_error).toString(), true); + return; + } + + boolean starting = HelperFunctions.prepareAndStartVpn(getActivity(), server); + + if (starting) { + if (requestTabListener != null) { + requestTabListener.onOpenStatusRequested(); + } + } + } + + private void showTestServers() { + ArrayList servers = new ArrayList<>(); + + VpnServerForList testServer = new VpnServerForList(); + testServer.lastUsed = new Date(); + testServer.countryCode = "au"; + testServer.name = "Server name"; + testServer.location = "Melbourne"; + testServer.pk = "024ec47420176680816e0406250e7156465e4531f5b26057c9f6297bb0303558c7"; + /* + testServer.congestion = 20; + testServer.congestionRating = ServerRatings.Gold; + testServer.latency = 123; + testServer.latencyRating = ServerRatings.Gold; + testServer.hops = 3; + */ + testServer.note = "Note"; + servers.add(testServer); + + testServer = new VpnServerForList(); + testServer.lastUsed = new Date(); + testServer.countryCode = "br"; + testServer.name = "Test server 14"; + testServer.location = "Rio de Janeiro"; + testServer.pk = "034ec47420176680816e0406250e7156465e4531f5b26057c9f6297bb0303558c7"; + /* + testServer.congestion = 20; + testServer.congestionRating = ServerRatings.Silver; + testServer.latency = 12345; + testServer.latencyRating = ServerRatings.Gold; + testServer.hops = 3; + */ + testServer.note = "Note"; + servers.add(testServer); + + testServer = new VpnServerForList(); + testServer.lastUsed = new Date(); + testServer.countryCode = "de"; + testServer.name = "Test server 20"; + testServer.location = "Berlin"; + testServer.pk = "044ec47420176680816e0406250e7156465e4531f5b26057c9f6297bb0303558c7"; + /* + testServer.congestion = 20; + testServer.congestionRating = ServerRatings.Gold; + testServer.latency = 123; + testServer.latencyRating = ServerRatings.Bronze; + testServer.hops = 7; + */ + servers.add(testServer); + + VPNServersPersistentData.getInstance().updateFromDiscovery(servers); + + if (serverSubscription != null) { + serverSubscription.dispose(); + } + + adapter.setData(new ArrayList<>(), listType); + noResultsContainer.setVisibility(View.GONE); + loadingAnimation.setVisibility(View.VISIBLE); + + serverSubscription = Observable.just(servers).delay(50, TimeUnit.MILLISECONDS).flatMap(serversList -> + VPNServersPersistentData.getInstance().history() + ).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(r -> { + loadingAnimation.setVisibility(View.GONE); + + ArrayList serversCopy = new ArrayList<>(servers); + + removeSavedData(serversCopy); + addSavedData(serversCopy); + adapter.setData(serversCopy, ServerLists.Public); + }); + + } + + private void addSavedData(ArrayList servers) { + ArrayList remove = new ArrayList(); + for (VpnServerForList server : servers) { + LocalServerData savedVersion = VPNServersPersistentData.getInstance().getSavedVersion(server.pk); + + if (savedVersion != null) { + server.customName = savedVersion.customName; + server.personalNote = savedVersion.personalNote; + server.inHistory = savedVersion.inHistory; + server.flag = savedVersion.flag; + server.enteredManually = savedVersion.enteredManually; + server.hasPassword = savedVersion.password != null && !savedVersion.password.equals(""); + } + + if (server.flag == ServerFlags.Blocked) { + remove.add(server); + } + } + + servers.removeAll(remove); + } + + private void removeSavedData(ArrayList servers) { + for (VpnServerForList server : servers) { + server.customName = null; + server.personalNote = null; + server.inHistory = false; + server.flag = ServerFlags.None; + server.enteredManually = false; + server.hasPassword = false; + } + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/VpnServerForList.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/VpnServerForList.java new file mode 100644 index 000000000..e96eaed8d --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/VpnServerForList.java @@ -0,0 +1,32 @@ +package com.skywire.skycoin.vpn.activities.servers; + +import com.skywire.skycoin.vpn.objects.ServerFlags; + +// TODO: for currently commented fields, must be deleted or reactivated depending on what happens to the fields. +// import com.skywire.skycoin.vpn.objects.ServerRatings; + +import java.util.Date; + +public class VpnServerForList { + public String countryCode; + public String name; + public String customName; + public String location; + public String pk; + public String note; + public String personalNote; + public Date lastUsed; + public boolean inHistory; + public ServerFlags flag; + public boolean hasPassword; + public boolean enteredManually; + + // TODO: for currently commented fields, must be deleted or reactivated depending on what happens to the fields. + /* + public double congestion; + public ServerRatings congestionRating; + public double latency; + public ServerRatings latencyRating; + public int hops; + */ +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/VpnServersAdapter.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/VpnServersAdapter.java new file mode 100644 index 000000000..8fe13b724 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/servers/VpnServersAdapter.java @@ -0,0 +1,559 @@ +package com.skywire.skycoin.vpn.activities.servers; + +import android.content.Context; +import android.content.res.Resources; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.controls.ManualServerModalWindow; +import com.skywire.skycoin.vpn.controls.options.OptionsItem; +import com.skywire.skycoin.vpn.controls.options.OptionsModalWindow; +import com.skywire.skycoin.vpn.extensible.ClickWithIndexEvent; +import com.skywire.skycoin.vpn.extensible.ListViewHolder; +import com.skywire.skycoin.vpn.helpers.BoxRowTypes; +import com.skywire.skycoin.vpn.helpers.HelperFunctions; +import com.skywire.skycoin.vpn.objects.LocalServerData; +import com.skywire.skycoin.vpn.vpn.VPNCoordinator; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; + +public class VpnServersAdapter extends RecyclerView.Adapter> implements ClickWithIndexEvent { + public interface VpnServerListEventListener { + void onVpnServerSelected(VpnServerForList selectedServer); + void onManualEntered(LocalServerData server); + void listHasElements(boolean hasElements, boolean emptyBecauseFilters); + void tabChangeRequested(ServerLists newListType); + } + + public enum SortableColumns { + AUTOMATIC, + DATE, + COUNTRY, + NAME, + LOCATION, + PK, + /* + // TODO: for currently commented fields, must be deleted or reactivated depending on what happens to the fields. + CONGESTION, + CONGESTION_RATING, + LATENCY, + LATENCY_RATING, + HOPS, + */ + NOTE; + + public static int getColumnNameId(SortableColumns column) { + if (column == SortableColumns.NAME) { + return R.string.tmp_select_server_name_label; + } else if (column == SortableColumns.DATE) { + return R.string.tmp_select_server_date_label; + } else if (column == SortableColumns.COUNTRY) { + return R.string.tmp_select_server_country_label; + } else if (column == SortableColumns.LOCATION) { + return R.string.tmp_select_server_location_label; + } else if (column == SortableColumns.PK) { + return R.string.tmp_select_server_public_key_label; + /* + // TODO: for currently commented fields, must be deleted or reactivated depending on what happens to the fields. + } else if (column == SortableColumns.CONGESTION) { + return R.string.tmp_select_server_congestion_label; + } else if (column == SortableColumns.CONGESTION_RATING) { + return R.string.tmp_select_server_congestion_rating_label; + } else if (column == SortableColumns.LATENCY) { + return R.string.tmp_select_server_latency_label; + } else if (column == SortableColumns.LATENCY_RATING) { + return R.string.tmp_select_server_latency_rating_label; + } else if (column == SortableColumns.HOPS) { + return R.string.tmp_select_server_hops_label; + */ + } else { + return R.string.tmp_select_server_note_label; + } + } + } + + private Context context; + private List data; + private List filteredData; + private ServerLists listType = ServerLists.Public; + private VpnServerListEventListener listEventListener; + private boolean showingRows; + private int initialServerIndex; + + private ArrayList filters; + private ConditionsList conditionsView; + + private ArrayList sortBy; + private ArrayList sortInverse; + + private ArrayList premadeButtons = new ArrayList<>(); + private ArrayList premadeRows = new ArrayList<>(); + private int lastUsedPremadeButtonIdex = 0; + + private ServerListOptions listOptionsView; + private ServerListTableHeader tableHeader; + + public VpnServersAdapter(Context context) { + this.context = context; + + int screenHeightInDP = (int)(Resources.getSystem().getDisplayMetrics().heightPixels / context.getResources().getDisplayMetrics().density); + showingRows = HelperFunctions.getWidthType(context) != HelperFunctions.WidthTypes.SMALL; + + if (!showingRows) { + int aproxButtonsToFillScreen = (int)Math.ceil((screenHeightInDP / ServerListButton.APROX_HEIGHT_DP) * 1.3); + for (int i = 0; i < aproxButtonsToFillScreen; i++) { + premadeButtons.add(createNewServerButton()); + } + initialServerIndex = 2; + } else { + int aproxButtonsToFillScreen = (int)Math.ceil((screenHeightInDP / ServerListTableRow.APROX_HEIGHT_DP) * 1.3); + for (int i = 0; i < aproxButtonsToFillScreen; i++) { + premadeRows.add(createNewServerRow()); + } + initialServerIndex = 3; + } + } + + public void setData(List data, ServerLists listType) { + this.data = data; + this.listType = listType; + + if (listOptionsView != null) { + listOptionsView.selectCorrectTab(listType); + } + + if (tableHeader != null) { + tableHeader.setListType(listType); + } + + processData(); + } + + private void processData() { + if (filters == null) { + filters = new ArrayList<>(); + sortBy = new ArrayList<>(); + sortInverse = new ArrayList<>(); + + for (int i = 0; i < 4; i++) { + filters.add(null); + sortBy.add(SortableColumns.AUTOMATIC); + sortInverse.add(false); + } + } + + FilterModalWindow.Filters currentFilters = filters.get(getCurrentListTypeIntVal()); + + if (currentFilters == null) { + filteredData = data; + } else { + filteredData = new ArrayList<>(); + + for (VpnServerForList element : data) { + boolean valid = true; + + if (valid && currentFilters.countryCode != null && !currentFilters.countryCode.equals("")) { + String elementVal = element.countryCode != null ? element.countryCode.toUpperCase() : ""; + if (!elementVal.equals(currentFilters.countryCode.toUpperCase())) { + valid = false; + } + } + + if (valid && currentFilters.name != null && !currentFilters.name.equals("")) { + if (!HelperFunctions.getServerName(element, "").toUpperCase().contains(currentFilters.name.toUpperCase())) { + valid = false; + } + } + + if (valid && currentFilters.location != null && !currentFilters.location.equals("")) { + String elementVal = element.location != null ? element.location.toUpperCase() : ""; + if (!elementVal.contains(currentFilters.location.toUpperCase())) { + valid = false; + } + } + + if (valid && currentFilters.pk != null && !currentFilters.pk.equals("")) { + if (!element.pk.toUpperCase().contains(currentFilters.pk.toUpperCase())) { + valid = false; + } + } + + if (valid && currentFilters.note != null && !currentFilters.note.equals("")) { + String elementVal1 = element.note != null ? element.note.toUpperCase() : ""; + String elementVal2 = element.personalNote != null ? element.personalNote.toUpperCase() : ""; + String filterVal = currentFilters.note.toUpperCase(); + if (!elementVal1.contains(filterVal) && !elementVal2.contains(filterVal)) { + valid = false; + } + } + + if (valid) { + filteredData.add(element); + } + } + } + + if (listEventListener != null) { + if (data.size() == 0) { + listEventListener.listHasElements(false, false); + } else { + if (filteredData.size() == 0) { + listEventListener.listHasElements(false, true); + } else { + listEventListener.listHasElements(true, false); + } + } + } + + sortList(); + } + + private void sortList() { + if (conditionsView != null) { + conditionsView.setConditions(sortBy.get(getCurrentListTypeIntVal()), sortInverse.get(getCurrentListTypeIntVal()), filters.get(getCurrentListTypeIntVal())); + } + + Comparator comparator = (a, b) -> { + SortableColumns sortColumn = sortBy.get(getCurrentListTypeIntVal()); + + if (sortColumn == SortableColumns.AUTOMATIC) { + if (listType == ServerLists.History) { + sortColumn = SortableColumns.DATE; + } else { + sortColumn = SortableColumns.COUNTRY; + } + } + + int result = 0; + if (sortColumn == SortableColumns.DATE) { + result = (int)((b.lastUsed.getTime() - a.lastUsed.getTime()) / 1000); + } else if (sortColumn == SortableColumns.COUNTRY) { + result = a.countryCode.compareTo(b.countryCode); + } else if (sortColumn == SortableColumns.NAME) { + result = HelperFunctions.getServerName(a, "").compareTo(HelperFunctions.getServerName(b, "")); + } else if (sortColumn == SortableColumns.LOCATION) { + result = (a.location != null ? a.location : "").compareTo((b.location != null ? b.location : "")); + } else if (sortColumn == SortableColumns.PK) { + result = (a.pk != null ? a.pk : "").compareTo((b.pk != null ? b.pk : "")); + /* + // TODO: for currently commented fields, must be deleted or reactivated depending on what happens to the fields. + } else if (sortColumn == SortableColumns.CONGESTION) { + result = (int)(a.congestion - b.congestion); + } else if (sortColumn == SortableColumns.CONGESTION_RATING) { + result = ServerRatings.getNumberForRating(b.congestionRating) - ServerRatings.getNumberForRating(a.congestionRating); + } else if (sortColumn == SortableColumns.LATENCY) { + result = (int)(a.latency - b.latency); + } else if (sortColumn == SortableColumns.LATENCY_RATING) { + result = ServerRatings.getNumberForRating(b.latencyRating) - ServerRatings.getNumberForRating(a.latencyRating); + } else if (sortColumn == SortableColumns.HOPS) { + result = (int)(a.hops - b.hops); + */ + } else if (sortColumn == SortableColumns.NOTE) { + String noteA = ((a.note != null ? a.note : "") + " " + (a.personalNote != null ? a.personalNote : "")).trim(); + String noteB = ((b.note != null ? b.note : "") + " " + (b.personalNote != null ? b.personalNote : "")).trim(); + if (noteA.equals("") && !noteB.equals("")) { + result = 1; + } else if (noteB.equals("") && !noteA.equals("")) { + result = -1; + } else { + result = noteA.compareTo(noteB); + } + } + + if (result == 0 && sortColumn != SortableColumns.NAME) { + result = HelperFunctions.getServerName(a, "").compareTo(HelperFunctions.getServerName(b, "")); + } + + if (result == 0 && sortColumn != SortableColumns.PK) { + result = (a.pk != null ? a.pk : "").compareTo((b.pk != null ? b.pk : "")); + } + + boolean mustSortInverse = sortInverse.get(getCurrentListTypeIntVal()); + + if (mustSortInverse) { + result *= -1; + } + + return result; + }; + + Collections.sort(filteredData, comparator); + + this.notifyDataSetChanged(); + } + + private int getCurrentListTypeIntVal() { + if (listType == ServerLists.Public) { + return 0; + } else if (listType == ServerLists.History) { + return 1; + } else if (listType == ServerLists.Favorites) { + return 2; + } + + return 3; + } + + public void setVpnServerListEventListener(VpnServerListEventListener listener) { + listEventListener = listener; + } + + @Override + public int getItemViewType(int position) { + if (position == 0) { + return 0; + } else if (position == 1) { + return 1; + } else if (position == 2 && showingRows) { + return 3; + } + + return 2; + } + + @NonNull + @Override + public ListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + if (viewType == 0) { + listOptionsView = new ServerListOptions(context); + listOptionsView.setClickWithIndexEventListener(this); + listOptionsView.selectCorrectTab(listType); + return new ListViewHolder<>(listOptionsView); + } else if (viewType == 1) { + conditionsView = new ConditionsList(context); + conditionsView.setConditions(sortBy.get(getCurrentListTypeIntVal()), sortInverse.get(getCurrentListTypeIntVal()), filters.get(getCurrentListTypeIntVal())); + + conditionsView.setClickEventListener(v -> { + if (conditionsView.showingFilters() && conditionsView.showingOrder()) { + ArrayList options = new ArrayList(); + + OptionsItem.SelectableOption option = new OptionsItem.SelectableOption(); + option.translatableLabelId = R.string.tmp_select_server_remove_filters_button; + options.add(option); + + option = new OptionsItem.SelectableOption(); + option.translatableLabelId = R.string.tmp_select_server_remove_custom_sorting_button; + options.add(option); + + option = new OptionsItem.SelectableOption(); + option.translatableLabelId = R.string.tmp_select_server_remove_both_button; + options.add(option); + + OptionsModalWindow modal = new OptionsModalWindow(context, null, options, (int selectedOption) -> { + if (selectedOption == 0 || selectedOption == 2) { + filters.set(getCurrentListTypeIntVal(), null); + } + if (selectedOption == 1 || selectedOption == 2) { + sortBy.set(getCurrentListTypeIntVal(), SortableColumns.AUTOMATIC); + sortInverse.set(getCurrentListTypeIntVal(), false); + } + + processData(); + }); + + modal.show(); + } else if (conditionsView.showingFilters()) { + filters.set(getCurrentListTypeIntVal(), null); + processData(); + } else if (conditionsView.showingOrder()) { + sortBy.set(getCurrentListTypeIntVal(), SortableColumns.AUTOMATIC); + sortInverse.set(getCurrentListTypeIntVal(), false); + processData(); + } + }); + + return new ListViewHolder<>(conditionsView); + } else if (viewType == 3) { + tableHeader = new ServerListTableHeader(context); + tableHeader.setListType(listType); + return new ListViewHolder<>(tableHeader); + } + + if (!showingRows) { + ServerListButton view; + if (lastUsedPremadeButtonIdex < premadeButtons.size()) { + view = premadeButtons.get(lastUsedPremadeButtonIdex); + lastUsedPremadeButtonIdex += 1; + } else { + view = createNewServerButton(); + } + + return new ListViewHolder<>(view); + } else { + ServerListTableRow view; + if (lastUsedPremadeButtonIdex < premadeRows.size()) { + view = premadeRows.get(lastUsedPremadeButtonIdex); + lastUsedPremadeButtonIdex += 1; + } else { + view = createNewServerRow(); + } + + return new ListViewHolder<>(view); + } + } + + private ServerListButton createNewServerButton() { + ServerListButton view = new ServerListButton(context); + view.setClickWithIndexEventListener(this); + return view; + } + + private ServerListTableRow createNewServerRow() { + ServerListTableRow view = new ServerListTableRow(context); + view.setClickWithIndexEventListener(this); + return view; + } + + @Override + public void onBindViewHolder(@NonNull ListViewHolder holder, int position) { + if (position >= initialServerIndex) { + position -= initialServerIndex; + + if (!showingRows) { + ((ServerListButton) holder.itemView).setIndex(position); + ((ServerListButton) holder.itemView).changeData(filteredData.get(position), listType); + + if (filteredData.size() == 1) { + ((ServerListButton) holder.itemView).setBoxRowType(BoxRowTypes.SINGLE); + } else if (position == 0) { + ((ServerListButton) holder.itemView).setBoxRowType(BoxRowTypes.TOP); + } else if (position == filteredData.size() - 1) { + ((ServerListButton) holder.itemView).setBoxRowType(BoxRowTypes.BOTTOM); + } else { + ((ServerListButton) holder.itemView).setBoxRowType(BoxRowTypes.MIDDLE); + } + } else { + ((ServerListTableRow) holder.itemView).setIndex(position); + ((ServerListTableRow) holder.itemView).changeData(filteredData.get(position), listType); + + if (position == filteredData.size() - 1) { + ((ServerListTableRow) holder.itemView).setBoxRowType(BoxRowTypes.BOTTOM); + } else { + ((ServerListTableRow) holder.itemView).setBoxRowType(BoxRowTypes.MIDDLE); + } + } + } + } + + @Override + public int getItemCount() { + if (!showingRows) { + return filteredData != null ? (filteredData.size() + 2) : 2; + } + + if (filteredData == null || filteredData.size() == 0) { + return 2; + } + return filteredData.size() + 3; + } + + @Override + public void onClickWithIndex(int index, Void data) { + if (listEventListener != null) { + if (index >= 0) { + listEventListener.onVpnServerSelected(this.filteredData.get(index)); + } else { + if (index <= ServerListOptions.showPublicIndex) { + if (index == ServerListOptions.showPublicIndex) { + listEventListener.tabChangeRequested(ServerLists.Public); + } else if (index == ServerListOptions.showHistoryIndex) { + listEventListener.tabChangeRequested(ServerLists.History); + } else if (index == ServerListOptions.showFavoritesIndex) { + listEventListener.tabChangeRequested(ServerLists.Favorites); + } else if (index == ServerListOptions.showBlockedIndex) { + listEventListener.tabChangeRequested(ServerLists.Blocked); + } + } else if (index == ServerListOptions.sortIndex) { + SortableColumns currentSortBy = sortBy.get(getCurrentListTypeIntVal()); + boolean currentSortInverse = sortInverse.get(getCurrentListTypeIntVal()); + + ArrayList optionValues = new ArrayList(); + if (listType == ServerLists.History) { + optionValues.add(SortableColumns.DATE); + } + optionValues.add(SortableColumns.NAME); + optionValues.add(SortableColumns.COUNTRY); + optionValues.add(SortableColumns.LOCATION); + optionValues.add(SortableColumns.PK); + /* + // TODO: for currently commented fields, must be deleted or reactivated depending on what happens to the fields. + if (listType == ServerLists.Public) { + optionValues.add(SortableColumns.CONGESTION); + optionValues.add(SortableColumns.CONGESTION_RATING); + optionValues.add(SortableColumns.LATENCY); + optionValues.add(SortableColumns.LATENCY_RATING); + optionValues.add(SortableColumns.HOPS); + } + */ + optionValues.add(SortableColumns.NOTE); + + ArrayList options = new ArrayList(); + OptionsItem.SelectableOption option = new OptionsItem.SelectableOption(); + option.translatableLabelId = R.string.tmp_select_server_automatic_label; + if (currentSortBy == SortableColumns.AUTOMATIC) { + option.icon = "\ue876"; + } + options.add(option); + + for(int i = 0; i < optionValues.size(); i++) { + option = new OptionsItem.SelectableOption(); + option.translatableLabelId = SortableColumns.getColumnNameId(optionValues.get(i)); + if (optionValues.get(i) == currentSortBy && !currentSortInverse) { + option.icon = "\ue876"; + } + options.add(option); + + option = new OptionsItem.SelectableOption(); + option.label = context.getText(SortableColumns.getColumnNameId(optionValues.get(i))) + " " + context.getText(R.string.tmp_select_server_reversed_suffix); + if (optionValues.get(i) == currentSortBy && currentSortInverse) { + option.icon = "\ue876"; + } + options.add(option); + } + + OptionsModalWindow modal = new OptionsModalWindow(context, context.getString(R.string.tmp_select_server_sort_title), options, (int selectedOption) -> { + if (selectedOption == 0) { + sortBy.set(getCurrentListTypeIntVal(), SortableColumns.AUTOMATIC); + sortInverse.set(getCurrentListTypeIntVal(), false); + } else { + selectedOption -= 1; + sortBy.set(getCurrentListTypeIntVal(), optionValues.get((int)(selectedOption / 2))); + sortInverse.set(getCurrentListTypeIntVal(), selectedOption % 2 != 0); + } + + sortList(); + }); + + modal.show(); + } else if (index == ServerListOptions.addIndex) { + if (VPNCoordinator.getInstance().isServiceRunning()) { + HelperFunctions.showToast(context.getText(R.string.tmp_select_server_running_error).toString(), true); + return; + } + + ManualServerModalWindow modal = new ManualServerModalWindow(context, server -> listEventListener.onManualEntered(server)); + modal.show(); + } else if (index == ServerListOptions.filterIndex) { + HashSet countries = new HashSet<>(); + for (VpnServerForList element : this.data) { + countries.add(element.countryCode); + } + + FilterModalWindow modal = new FilterModalWindow(context, countries, filters.get(getCurrentListTypeIntVal()), newFilters -> { + filters.set(getCurrentListTypeIntVal(), newFilters); + processData(); + }); + modal.show(); + } + } + } + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/settings/CustomDnsModalWindow.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/settings/CustomDnsModalWindow.java new file mode 100644 index 000000000..9a5639faa --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/settings/CustomDnsModalWindow.java @@ -0,0 +1,108 @@ +package com.skywire.skycoin.vpn.activities.settings; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.View; +import android.view.Window; +import android.view.inputmethod.EditorInfo; +import android.widget.EditText; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.controls.ModalWindowButton; +import com.skywire.skycoin.vpn.extensible.ClickEvent; +import com.skywire.skycoin.vpn.helpers.HelperFunctions; +import com.skywire.skycoin.vpn.vpn.VPNGeneralPersistentData; + +import java.util.regex.Matcher; + +import static androidx.core.util.PatternsCompat.IP_ADDRESS; + +public class CustomDnsModalWindow extends Dialog implements ClickEvent { + public interface Confirmed { + void confirmed(String newIp); + } + + private EditText editValue; + private ModalWindowButton buttonCancel; + private ModalWindowButton buttonConfirm; + + private Confirmed event; + + public CustomDnsModalWindow(Context ctx, Confirmed event) { + super(ctx); + + this.event = event; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_NO_TITLE); + setContentView(R.layout.view_settings_dns_modal); + + editValue = findViewById(R.id.editValue); + buttonCancel = findViewById(R.id.buttonCancel); + buttonConfirm = findViewById(R.id.buttonConfirm); + + String currentServer = VPNGeneralPersistentData.getCustomDns(); + if (currentServer != null) { + editValue.setText(currentServer); + } + + editValue.setOnEditorActionListener((v, actionId, event) -> { + if ( + actionId == EditorInfo.IME_ACTION_DONE || + (event != null && event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_ENTER) + ) { + makeChange(); + + return true; + } + + return false; + }); + + editValue.setSelection(editValue.getText().length()); + + buttonCancel.setClickEventListener(this); + buttonConfirm.setClickEventListener(this); + + HelperFunctions.configureModalWindow(this); + } + + @Override + public void onClick(View view) { + if (view.getId() == R.id.buttonConfirm) { + makeChange(); + } else { + dismiss(); + } + } + + private void makeChange() { + boolean valid = false; + String ip = null; + + if (editValue.getText() == null || editValue.getText().toString().trim().length() == 0) { + valid = true; + } else { + ip = editValue.getText().toString().trim(); + Matcher matcher = IP_ADDRESS.matcher(ip); + if (matcher.matches()) { + valid = true; + } + } + + if (valid) { + if (event != null) { + event.confirmed(ip); + } + + dismiss(); + } else { + HelperFunctions.showToast(getContext().getString(R.string.tmp_dns_validation_error), true); + } + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/settings/SettingsActivity.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/settings/SettingsActivity.java new file mode 100644 index 000000000..e079385c5 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/settings/SettingsActivity.java @@ -0,0 +1,196 @@ +package com.skywire.skycoin.vpn.activities.settings; + +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import com.skywire.skycoin.vpn.activities.apps.AppsActivity; +import com.skywire.skycoin.vpn.controls.options.OptionsItem; +import com.skywire.skycoin.vpn.controls.options.OptionsModalWindow; +import com.skywire.skycoin.vpn.extensible.ClickEvent; +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.helpers.Globals; +import com.skywire.skycoin.vpn.helpers.HelperFunctions; +import com.skywire.skycoin.vpn.vpn.VPNCoordinator; +import com.skywire.skycoin.vpn.vpn.VPNGeneralPersistentData; +import com.skywire.skycoin.vpn.vpn.VPNServersPersistentData; + +import java.util.ArrayList; +import java.util.HashSet; + +public class SettingsActivity extends Fragment implements ClickEvent { + private SettingsOption optionApps; + private SettingsOption optionShowIp; + private SettingsOption optionKillSwitch; + private SettingsOption optionResetAfterErrors; + private SettingsOption optionProtectBeforeConnecting; + private SettingsOption optionStartOnBoot; + private SettingsOption optionDataUnits; + private SettingsOption optionDns; + + // Units that must be used for displaying the data stats. + private Globals.DataUnits dataUnitsOption = VPNGeneralPersistentData.getDataUnits(); + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); + + return inflater.inflate(R.layout.activity_settings, container, true); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + optionApps = view.findViewById(R.id.optionApps); + optionShowIp = view.findViewById(R.id.optionShowIp); + optionKillSwitch = view.findViewById(R.id.optionKillSwitch); + optionResetAfterErrors = view.findViewById(R.id.optionResetAfterErrors); + optionProtectBeforeConnecting = view.findViewById(R.id.optionProtectBeforeConnecting); + optionStartOnBoot = view.findViewById(R.id.optionStartOnBoot); + optionDataUnits = view.findViewById(R.id.optionDataUnits); + optionDns = view.findViewById(R.id.optionDns); + + optionShowIp.setChecked(VPNGeneralPersistentData.getShowIpActivated()); + optionKillSwitch.setChecked(VPNGeneralPersistentData.getKillSwitchActivated()); + optionResetAfterErrors.setChecked(VPNGeneralPersistentData.getMustRestartVpn()); + optionProtectBeforeConnecting.setChecked(VPNGeneralPersistentData.getProtectBeforeConnected()); + optionStartOnBoot.setChecked(VPNGeneralPersistentData.getStartOnBoot()); + + optionApps.setClickEventListener(this); + optionShowIp.setClickEventListener(this); + optionKillSwitch.setClickEventListener(this); + optionResetAfterErrors.setClickEventListener(this); + optionProtectBeforeConnecting.setClickEventListener(this); + optionStartOnBoot.setClickEventListener(this); + optionDataUnits.setClickEventListener(this); + optionDns.setClickEventListener(this); + + optionDataUnits.setDescription(getUnitsOptionText(dataUnitsOption), null); + + setDnsOptionText(VPNGeneralPersistentData.getCustomDns()); + } + + @Override + public void onResume() { + super.onResume(); + + Globals.AppFilteringModes appsMode = VPNGeneralPersistentData.getAppsSelectionMode(); + if (appsMode == Globals.AppFilteringModes.PROTECT_ALL) { + optionApps.setDescription(R.string.tmp_options_apps_description, null); + optionApps.setChecked(false); + optionApps.changeAlertIconVisibility(false); + } else { + HashSet selectedApps = HelperFunctions.filterAvailableApps(VPNGeneralPersistentData.getAppList(new HashSet<>())); + + if (appsMode == Globals.AppFilteringModes.PROTECT_SELECTED) { + optionApps.setDescription(R.string.tmp_options_apps_include_description, selectedApps.size() + ""); + } else if (appsMode == Globals.AppFilteringModes.IGNORE_SELECTED) { + optionApps.setDescription(R.string.tmp_options_apps_exclude_description, selectedApps.size() + ""); + } + + optionApps.setChecked(true); + optionApps.changeAlertIconVisibility(true); + } + } + + /** + * Gets the ID of the string for a data units selection. + */ + private int getUnitsOptionText(Globals.DataUnits units) { + if (units == Globals.DataUnits.OnlyBits) { + return R.string.tmp_options_data_units_only_bits; + } else if (units == Globals.DataUnits.OnlyBytes) { + return R.string.tmp_options_data_units_only_bytes; + } + + return R.string.tmp_options_data_units_bits_speed_and_bytes_volume; + } + + private void setDnsOptionText(String customIp) { + if (customIp == null || customIp.trim().length() == 0) { + optionDns.setDescription(R.string.tmp_options_dns_default, null); + optionDns.changeAlertIconVisibility(false); + } else { + optionDns.setDescription(R.string.tmp_options_dns_description, customIp); + optionDns.changeAlertIconVisibility(true); + } + } + + @Override + public void onClick(View view) { + if (view.getId() == R.id.optionDataUnits) { + ArrayList options = new ArrayList(); + Globals.DataUnits[] unitOptions = new Globals.DataUnits[3]; + unitOptions[0] = Globals.DataUnits.BitsSpeedAndBytesVolume; + unitOptions[1] = Globals.DataUnits.OnlyBytes; + unitOptions[2] = Globals.DataUnits.OnlyBits; + + for (Globals.DataUnits unitOption : unitOptions) { + OptionsItem.SelectableOption option = new OptionsItem.SelectableOption(); + option.icon = dataUnitsOption == unitOption ? "\ue876" : null; + option.translatableLabelId = getUnitsOptionText(unitOption); + options.add(option); + } + + OptionsModalWindow modal = new OptionsModalWindow(getContext(), null, options, (int selectedOption) -> { + dataUnitsOption = unitOptions[selectedOption]; + optionDataUnits.setDescription(getUnitsOptionText(dataUnitsOption), null); + VPNGeneralPersistentData.setDataUnits(dataUnitsOption); + }); + modal.show(); + + return; + } + + if (VPNCoordinator.getInstance().isServiceRunning()) { + HelperFunctions.showToast(getContext().getText(R.string.general_server_running_error).toString(), true); + + return; + } + + if (view.getId() == R.id.optionApps) { + Intent intent = new Intent(getContext(), AppsActivity.class); + startActivity(intent); + + return; + } + + if (view.getId() == R.id.optionDns) { + CustomDnsModalWindow modal = new CustomDnsModalWindow(getContext(), (String newIp) -> { + VPNGeneralPersistentData.setCustomDns(newIp); + setDnsOptionText(newIp); + + HelperFunctions.showToast(getContext().getString(R.string.tmp_dns_changes_made_confirmation), true); + }); + modal.show(); + } + + if (view.getId() == R.id.optionStartOnBoot && VPNServersPersistentData.getInstance().getCurrentServer() == null) { + HelperFunctions.showToast(getContext().getText(R.string.tmp_options_start_on_boot_without_server_error).toString(), true); + + return; + } + + ((SettingsOption)view).setChecked(!((SettingsOption)view).isChecked()); + + if (view.getId() == R.id.optionShowIp) { + VPNGeneralPersistentData.setShowIpActivated(((SettingsOption)view).isChecked()); + } else if (view.getId() == R.id.optionKillSwitch) { + VPNGeneralPersistentData.setKillSwitchActivated(((SettingsOption)view).isChecked()); + } else if (view.getId() == R.id.optionResetAfterErrors) { + VPNGeneralPersistentData.setMustRestartVpn(((SettingsOption)view).isChecked()); + } else if (view.getId() == R.id.optionProtectBeforeConnecting) { + VPNGeneralPersistentData.setProtectBeforeConnected(((SettingsOption)view).isChecked()); + } else { + VPNGeneralPersistentData.setStartOnBoot(((SettingsOption)view).isChecked()); + } + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/settings/SettingsOption.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/settings/SettingsOption.java new file mode 100644 index 000000000..8aaf37e96 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/settings/SettingsOption.java @@ -0,0 +1,106 @@ +package com.skywire.skycoin.vpn.activities.settings; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.widget.CheckBox; +import android.widget.TextView; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.controls.BoxRowLayout; +import com.skywire.skycoin.vpn.extensible.ButtonBase; +import com.skywire.skycoin.vpn.helpers.BoxRowTypes; + +public class SettingsOption extends ButtonBase { + private BoxRowLayout mainLayout; + private TextView textAlertIcon; + private TextView textName; + private TextView textDescription; + private CheckBox checkSelected; + + public SettingsOption(Context context) { + super(context); + } + public SettingsOption(Context context, AttributeSet attrs) { + super(context, attrs); + } + public SettingsOption(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void Initialize(Context context, AttributeSet attrs) { + LayoutInflater inflater = (LayoutInflater)context.getSystemService (Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_settings_list_item, this, true); + + mainLayout = this.findViewById (R.id.mainLayout); + textAlertIcon = this.findViewById (R.id.textAlertIcon); + textName = this.findViewById (R.id.textName); + textDescription = this.findViewById (R.id.textDescription); + checkSelected = this.findViewById (R.id.checkSelected); + + int type = 1; + String name = ""; + String description = ""; + + if (attrs != null) { + TypedArray attributes = getContext().getTheme().obtainStyledAttributes( + attrs, + R.styleable.SettingsOption, + 0, 0 + ); + + type = attributes.getInteger(R.styleable.SettingsOption_box_row_type, 1); + name = attributes.getString(R.styleable.SettingsOption_title); + description = attributes.getString(R.styleable.SettingsOption_description); + + boolean hideCheckbox = attributes.getBoolean(R.styleable.SettingsOption_hide_checkbox, false); + if (hideCheckbox) { + checkSelected.setVisibility(GONE); + } + + attributes.recycle(); + } + + textName.setText(name); + textDescription.setText(description); + + if (type == 0) { + mainLayout.setType(BoxRowTypes.TOP); + } else if (type == 1) { + mainLayout.setType(BoxRowTypes.MIDDLE); + } else if (type == 2) { + mainLayout.setType(BoxRowTypes.BOTTOM); + } else if (type == 3) { + mainLayout.setType(BoxRowTypes.SINGLE); + } + + textAlertIcon.setVisibility(GONE); + + setClickableBoxView(mainLayout); + } + + public void setChecked(boolean checked) { + checkSelected.setChecked(checked); + } + public boolean isChecked() { + return checkSelected.isChecked(); + } + + public void setDescription(int resource, String param) { + if (param == null) { + textDescription.setText(resource); + } else { + textDescription.setText(String.format(getResources().getString(resource), param)); + } + } + + public void changeAlertIconVisibility(boolean visible) { + if (visible) { + textAlertIcon.setVisibility(VISIBLE); + } else { + textAlertIcon.setVisibility(GONE); + } + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/start/MapBackground.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/start/MapBackground.java new file mode 100644 index 000000000..142647ecd --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/start/MapBackground.java @@ -0,0 +1,139 @@ +package com.skywire.skycoin.vpn.activities.start; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.util.AttributeSet; +import android.view.View; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; + +import com.skywire.skycoin.vpn.R; + +public class MapBackground extends View { + public MapBackground(Context context) { + super(context); + Initialize(context, null); + } + public MapBackground(Context context, AttributeSet attrs) { + super(context, attrs); + Initialize(context, attrs); + } + public MapBackground(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + Initialize(context, attrs); + } + + private BitmapDrawable bitmapDrawable; + private float proportion = 1; + private Rect drawableArea = new Rect(0, 0,1, 1); + private int widthSize; + private boolean finished = false; + private ObjectAnimator animation; + + private void Initialize (Context context, AttributeSet attrs) { + Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.map_phones); + bitmapDrawable = new BitmapDrawable(context.getResources(), bitmap); + bitmapDrawable.setAlpha(25); + + proportion = (float)bitmap.getWidth() / (float)bitmap.getHeight(); + } + + public void pauseAnimation() { + if (animation != null) { + animation.pause(); + } + } + + public void resumeAnimation() { + if (animation != null) { + animation.resume(); + } + } + + public void cancelAnimation() { + finished = true; + stopAnimation(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + if (widthSize != drawableArea.width() || heightSize != drawableArea.height()) { + setValues(widthSize, heightSize); + } + + setMeasuredDimension(drawableArea.width(), drawableArea.height()); + } + + @Override + protected void onDraw(Canvas canvas) { + bitmapDrawable.draw(canvas); + super.onDraw(canvas); + } + + private void setValues(int width, int height) { + if (finished) { + return; + } + + drawableArea = new Rect(0, 0, (int) (height * proportion), height); + bitmapDrawable.setBounds(drawableArea); + + stopAnimation(); + selectPosition(); + startAnimation(true); + } + + private void selectPosition() { + int max = drawableArea.width() - widthSize; + this.setTranslationX(-(int)Math.round(Math.random() * max)); + invalidate(); + } + + private void startAnimation(boolean appear) { + animation = ObjectAnimator.ofFloat(this, "alpha", appear ? 0 : 1, appear ? 1 : 0); + animation.setDuration(800); + animation.setInterpolator(appear ? new DecelerateInterpolator() : new AccelerateInterpolator()); + if (!appear) { + animation.setStartDelay(15000); + } + + animation.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { } + @Override + public void onAnimationCancel(Animator animation) { } + @Override + public void onAnimationRepeat(Animator animation) { } + + @Override + public void onAnimationEnd(Animator anim) { + stopAnimation(); + if (appear) { + startAnimation(false); + } else { + selectPosition(); + startAnimation(true); + } + } + }); + + animation.start(); + } + + private void stopAnimation() { + if (animation != null) { + animation.removeAllListeners(); + animation.cancel(); + animation = null; + } + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/start/StartActivity.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/start/StartActivity.java new file mode 100644 index 000000000..36299e3b6 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/start/StartActivity.java @@ -0,0 +1,297 @@ +package com.skywire.skycoin.vpn.activities.start; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.activities.index.IndexPageAdapter; +import com.skywire.skycoin.vpn.activities.start.connected.StartViewConnected; +import com.skywire.skycoin.vpn.activities.start.disconnected.StartViewDisconnected; +import com.skywire.skycoin.vpn.helpers.HelperFunctions; +import com.skywire.skycoin.vpn.vpn.VPNCoordinator; + +import java.util.concurrent.TimeUnit; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public class StartActivity extends Fragment { + private enum SimpleVpnStates { + Unknown, + Running, + Stopped, + } + + private FrameLayout mainContainer; + private MapBackground background; + + private StartViewDisconnected viewDisconnected; + private StartViewConnected viewConnected; + + private SimpleVpnStates vpnState = SimpleVpnStates.Unknown; + private ObjectAnimator animation; + private ObjectAnimator positionAnimation; + private SimpleVpnStates animationDestination = SimpleVpnStates.Unknown; + + private IndexPageAdapter.RequestTabListener requestTabListener; + private Disposable serviceSubscription; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); + + return inflater.inflate(R.layout.activity_start, container, true); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + mainContainer = view.findViewById(R.id.mainContainer); + background = view.findViewById(R.id.background); + + if (!HelperFunctions.showBackgroundForVerticalScreen()) { + background.setVisibility(View.GONE); + } + } + + public void setRequestTabListener(IndexPageAdapter.RequestTabListener listener) { + requestTabListener = listener; + if (viewDisconnected != null) { + viewDisconnected.setRequestTabListener(listener); + } + } + + @Override + public void onStart() { + super.onStart(); + + serviceSubscription = VPNCoordinator.getInstance().getEventsObservable().subscribe(state -> { + if (state.state.val() < 10) { + if (vpnState == SimpleVpnStates.Unknown) { + vpnState = SimpleVpnStates.Stopped; + configureViewDisconnected(); + } else { + vpnState = SimpleVpnStates.Stopped; + startInitialAnimation(SimpleVpnStates.Stopped); + } + } else { + if (vpnState == SimpleVpnStates.Unknown) { + vpnState = SimpleVpnStates.Running; + configureViewConnected(); + } else { + vpnState = SimpleVpnStates.Running; + startInitialAnimation(SimpleVpnStates.Running); + } + } + }); + } + + private void configureViewDisconnected() { + if (viewDisconnected == null) { + if (viewConnected != null) { + mainContainer.removeView(viewConnected); + viewConnected.close(); + viewConnected = null; + } + + viewDisconnected = new StartViewDisconnected(getContext()); + viewDisconnected.setParentActivity(getActivity()); + if (requestTabListener != null) { + viewDisconnected.setRequestTabListener(requestTabListener); + } + + mainContainer.addView(viewDisconnected); + viewDisconnected.startAnimation(); + } + } + + private void configureViewConnected() { + if (viewConnected == null) { + if (viewDisconnected != null) { + mainContainer.removeView(viewDisconnected); + viewDisconnected.close(); + viewDisconnected = null; + } + + viewConnected = new StartViewConnected(getContext()); + mainContainer.addView(viewConnected); + } + } + + private void startInitialAnimation(SimpleVpnStates desiredDestination) { + if (animation != null || desiredDestination == SimpleVpnStates.Unknown) { + return; + } + if (desiredDestination == SimpleVpnStates.Running && viewConnected != null) { + return; + } + if (desiredDestination == SimpleVpnStates.Stopped && viewDisconnected != null) { + return; + } + + animationDestination = desiredDestination; + + View viewToAnimate; + if (desiredDestination == SimpleVpnStates.Running) { + viewToAnimate = viewDisconnected; + } else { + viewToAnimate = viewConnected; + } + + animate(viewToAnimate, true); + } + + private void startFinalAnimation() { + View viewToAnimate; + if (animationDestination == SimpleVpnStates.Running) { + configureViewConnected(); + viewToAnimate = viewConnected; + } else { + configureViewDisconnected(); + viewToAnimate = viewDisconnected; + } + + animate(viewToAnimate, false); + } + + private void animate(View viewToAnimate, boolean isInitialAnimation) { + if (animation != null) { + animation.cancel(); + } + if (positionAnimation != null) { + positionAnimation.cancel(); + } + + float initialPosition; + float finalPosition; + if (animationDestination == SimpleVpnStates.Running) { + if (isInitialAnimation) { + initialPosition = 0; + finalPosition = 20 * getContext().getResources().getDisplayMetrics().density; + } else { + initialPosition = -20 * getContext().getResources().getDisplayMetrics().density; + finalPosition = 0; + } + } else { + if (isInitialAnimation) { + initialPosition = 0; + finalPosition = -20 * getContext().getResources().getDisplayMetrics().density; + } else { + initialPosition = 20 * getContext().getResources().getDisplayMetrics().density; + finalPosition = 0; + } + } + + long duration = 200; + + positionAnimation = ObjectAnimator.ofFloat(viewToAnimate, "translationY", initialPosition, finalPosition); + positionAnimation.setDuration(duration); + positionAnimation.setInterpolator(isInitialAnimation ? new AccelerateInterpolator() : new DecelerateInterpolator()); + positionAnimation.start(); + + animation = ObjectAnimator.ofFloat(viewToAnimate, "alpha", isInitialAnimation ? 1 : 0, isInitialAnimation ? 0 : 1); + animation.setDuration(duration); + animation.setInterpolator(isInitialAnimation ? new AccelerateInterpolator() : new DecelerateInterpolator()); + + animation.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { } + @Override + public void onAnimationCancel(Animator animation) { } + @Override + public void onAnimationRepeat(Animator animation) { } + + @Override + public void onAnimationEnd(Animator animation) { + if (isInitialAnimation) { + Observable.just(1).delay(50, TimeUnit.MILLISECONDS) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(v -> startFinalAnimation()); + } else { + finishAnimations(); + animationDestination = SimpleVpnStates.Unknown; + + if (vpnState == SimpleVpnStates.Running && viewConnected == null) { + startInitialAnimation(SimpleVpnStates.Running); + } else if (vpnState == SimpleVpnStates.Stopped && viewDisconnected == null) { + startInitialAnimation(SimpleVpnStates.Stopped); + } + } + } + }); + + animation.start(); + } + + private void finishAnimations() { + animation.cancel(); + animation = null; + + positionAnimation.cancel(); + positionAnimation = null; + } + + @Override + public void onResume() { + super.onResume(); + + background.resumeAnimation(); + if (viewDisconnected != null) { + viewDisconnected.startAnimation(); + viewDisconnected.updateRightBar(); + } + if (viewConnected != null) { + viewConnected.continueUpdatingStats(); + viewConnected.updateRightBar(); + } + } + + @Override + public void onPause() { + super.onPause(); + + background.pauseAnimation(); + if (viewDisconnected != null) { + viewDisconnected.stopAnimation(); + } + if (viewConnected != null) { + viewConnected.pauseUpdatingStats(); + } + } + + @Override + public void onStop() { + super.onStop(); + serviceSubscription.dispose(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + + background.cancelAnimation(); + + if (viewDisconnected != null) { + viewDisconnected.close(); + } + if (viewConnected != null) { + viewConnected.close(); + } + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/start/StartViewRightPanel.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/start/StartViewRightPanel.java new file mode 100644 index 000000000..f20ea4fa7 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/start/StartViewRightPanel.java @@ -0,0 +1,329 @@ +package com.skywire.skycoin.vpn.activities.start; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.content.res.TypedArray; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.RelativeSizeSpan; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.activities.apps.AppsActivity; +import com.skywire.skycoin.vpn.activities.servers.ServerLists; +import com.skywire.skycoin.vpn.activities.servers.ServersActivity; +import com.skywire.skycoin.vpn.controls.ClickableLinearLayout; +import com.skywire.skycoin.vpn.controls.ServerName; +import com.skywire.skycoin.vpn.extensible.ClickEvent; +import com.skywire.skycoin.vpn.helpers.AlphaSpan; +import com.skywire.skycoin.vpn.helpers.Globals; +import com.skywire.skycoin.vpn.helpers.HelperFunctions; +import com.skywire.skycoin.vpn.helpers.MaterialFontSpan; +import com.skywire.skycoin.vpn.network.ApiClient; +import com.skywire.skycoin.vpn.objects.LocalServerData; +import com.skywire.skycoin.vpn.vpn.VPNGeneralPersistentData; +import com.skywire.skycoin.vpn.vpn.VPNServersPersistentData; + +import java.io.Closeable; +import java.util.Date; +import java.util.HashSet; +import java.util.concurrent.TimeUnit; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public class StartViewRightPanel extends FrameLayout implements ClickEvent, Closeable { + public StartViewRightPanel(Context context) { + super(context); + Initialize(context, null); + } + public StartViewRightPanel(Context context, AttributeSet attrs) { + super(context, attrs); + Initialize(context, attrs); + } + public StartViewRightPanel(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + Initialize(context, attrs); + } + + private final int retryDelay = 20000; + + private TextView textWaitingIp; + private TextView textIp; + private TextView textWaitingCountry; + private TextView textCountry; + private TextView textRemotePk; + private TextView textLocalPk; + private TextView textAppProtection; + private ServerName serverName; + private ClickableLinearLayout ipClickableLayout; + private ClickableLinearLayout serverClickableLayout; + private ClickableLinearLayout remotePkClickableLayout; + private ClickableLinearLayout localPkClickableLayout; + private ClickableLinearLayout appProtectionClickableLayout; + private LinearLayout loadingIpContainer; + private LinearLayout ipContainer; + private LinearLayout countryContainer; + private LinearLayout bottomPartContainer; + private ProgressBar progressCountry; + + private LocalServerData currentServer; + + private String previousIp; + private String currentIp; + private String previousCountry; + private Date lastIpRefresDate; + + private Disposable serverSubscription; + private Disposable ipSubscription; + + protected void Initialize (Context context, AttributeSet attrs) { + LayoutInflater inflater = (LayoutInflater)context.getSystemService (Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_start_right_panel, this, true); + + textWaitingIp = findViewById(R.id.textWaitingIp); + textIp = findViewById(R.id.textIp); + textWaitingCountry = findViewById(R.id.textWaitingCountry); + textCountry = findViewById(R.id.textCountry); + textRemotePk = findViewById(R.id.textRemotePk); + textLocalPk = findViewById(R.id.textLocalPk); + textAppProtection = findViewById(R.id.textAppProtection); + serverName = findViewById(R.id.serverName); + ipClickableLayout = findViewById(R.id.ipClickableLayout); + serverClickableLayout = findViewById(R.id.serverClickableLayout); + remotePkClickableLayout = findViewById(R.id.remotePkClickableLayout); + localPkClickableLayout = findViewById(R.id.localPkClickableLayout); + appProtectionClickableLayout = findViewById(R.id.appProtectionClickableLayout); + loadingIpContainer = findViewById(R.id.loadingIpContainer); + ipContainer = findViewById(R.id.ipContainer); + countryContainer = findViewById(R.id.countryContainer); + bottomPartContainer = findViewById(R.id.bottomPartContainer); + progressCountry = findViewById(R.id.progressCountry); + + ipClickableLayout.setClickEventListener(this); + serverClickableLayout.setClickEventListener(this); + remotePkClickableLayout.setClickEventListener(this); + localPkClickableLayout.setClickEventListener(this); + appProtectionClickableLayout.setClickEventListener(this); + + localPkClickableLayout.setVisibility(View.GONE); + ipClickableLayout.setVisibility(View.GONE); + ipContainer.setVisibility(View.GONE); + countryContainer.setVisibility(View.GONE); + + if (attrs != null) { + TypedArray attributes = context.getTheme().obtainStyledAttributes( + attrs, + R.styleable.StartViewRightPanel, + 0, 0 + ); + + if (attributes.getBoolean(R.styleable.StartViewRightPanel_hide_bottom_part, false)) { + bottomPartContainer.setVisibility(GONE); + } + + attributes.recycle(); + } + + if (!isInEditMode()) { + updateData(); + + if (!VPNGeneralPersistentData.getShowIpActivated()) { + textWaitingIp.setText(R.string.tmp_status_connected_ip_option_disabled); + textWaitingCountry.setText(R.string.tmp_status_connected_ip_option_disabled); + } + } + } + + public void updateData() { + if (serverSubscription == null) { + serverSubscription = VPNServersPersistentData.getInstance().getCurrentServerObservable().subscribe(server -> { + currentServer = server; + serverName.setServer(ServersActivity.convertLocalServerData(currentServer), ServerLists.History, true); + putTextWithIcon(textRemotePk, currentServer.pk, " \ue14d"); + }); + } + + Globals.AppFilteringModes selectedMode = VPNGeneralPersistentData.getAppsSelectionMode(); + if (selectedMode != Globals.AppFilteringModes.PROTECT_ALL) { + HashSet selectedApps = HelperFunctions.filterAvailableApps(VPNGeneralPersistentData.getAppList(new HashSet<>())); + + if (selectedApps.size() > 0) { + appProtectionClickableLayout.setVisibility(VISIBLE); + + String text; + if (selectedMode == Globals.AppFilteringModes.PROTECT_SELECTED) { + text = getContext().getString(R.string.tmp_status_connected_protecting_selected_apps); + } else { + text = getContext().getString(R.string.tmp_status_connected_ignoring_selected_apps); + } + + putTextWithIcon(textAppProtection, text, " \ue8f4"); + } else { + appProtectionClickableLayout.setVisibility(GONE); + } + } else { + appProtectionClickableLayout.setVisibility(GONE); + } + } + + public void putInWaitingForVpnState() { + cancelIpCheck(); + + ipClickableLayout.setVisibility(GONE); + loadingIpContainer.setVisibility(VISIBLE); + + textWaitingIp.setVisibility(VISIBLE); + textWaitingCountry.setVisibility(VISIBLE); + ipContainer.setVisibility(View.GONE); + countryContainer.setVisibility(View.GONE); + } + + public void refreshIpData() { + getIp(0); + } + + private void getIp(int delayMs) { + if (!VPNGeneralPersistentData.getShowIpActivated()) { + return; + } + + cancelIpCheck(); + + ipClickableLayout.setVisibility(GONE); + loadingIpContainer.setVisibility(VISIBLE); + + textWaitingIp.setVisibility(GONE); + textWaitingCountry.setVisibility(GONE); + progressCountry.setVisibility(VISIBLE); + ipContainer.setVisibility(View.VISIBLE); + countryContainer.setVisibility(View.VISIBLE); + textIp.setText("---"); + textCountry.setText("---"); + + ipSubscription = Observable.just(0).delay(delayMs, TimeUnit.MILLISECONDS).flatMap(v -> ApiClient.getCurrentIp()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(response -> { + if (response.body() != null) { + lastIpRefresDate = new Date(); + + ipClickableLayout.setVisibility(VISIBLE); + loadingIpContainer.setVisibility(GONE); + + currentIp = response.body().ip; + textIp.setText(currentIp); + + if (currentIp.equals(previousIp) && previousCountry != null) { + textCountry.setText(previousCountry); + progressCountry.setVisibility(GONE); + } else { + getIpCountry(0); + } + + previousIp = currentIp; + } else { + getIp(retryDelay); + } + }, err -> { + getIp(retryDelay); + }); + } + + private void getIpCountry(int delayMs) { + if (!VPNGeneralPersistentData.getShowIpActivated()) { + return; + } + + ipSubscription.dispose(); + + ipSubscription = Observable.just(0).delay(delayMs, TimeUnit.MILLISECONDS).flatMap(v -> ApiClient.getIpCountry(currentIp)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(response -> { + if (response.body() != null) { + progressCountry.setVisibility(GONE); + + String[] dataParts = response.body().split(";"); + if (dataParts.length == 4) { + textCountry.setText(dataParts[3]); + } else { + textCountry.setText(getContext().getText(R.string.general_unknown)); + } + + previousCountry = textCountry.getText().toString(); + } else { + getIpCountry(retryDelay); + } + }, err -> { + getIpCountry(retryDelay); + }); + } + + private void cancelIpCheck() { + if (ipSubscription != null) { + ipSubscription.dispose(); + } + } + + private void putTextWithIcon(TextView textView, String text, String iconText) { + MaterialFontSpan materialFontSpan = new MaterialFontSpan(getContext()); + RelativeSizeSpan relativeSizeSpan = new RelativeSizeSpan(0.75f); + AlphaSpan alphaSpan = new AlphaSpan(128); + + SpannableStringBuilder finalText = new SpannableStringBuilder(text.toString() + iconText); + finalText.setSpan(materialFontSpan, finalText.length() - iconText.length(), finalText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + finalText.setSpan(relativeSizeSpan, finalText.length() - iconText.length(), finalText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + finalText.setSpan(alphaSpan, finalText.length() - iconText.length(), finalText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + textView.setText(finalText); + } + + @Override + public void onClick(View view) { + if (view.getId() == R.id.ipClickableLayout) { + long msToWait = 10000; + long elapsedTime = (new Date()).getTime() - lastIpRefresDate.getTime(); + + if (elapsedTime < msToWait) { + HelperFunctions.showToast(String.format( + getContext().getText(R.string.tmp_status_connected_ip_refresh_time_warning).toString(), + HelperFunctions.zeroDecimalsFormatter.format(Math.ceil((msToWait - elapsedTime)) / 1000d) + ), true); + } else { + this.refreshIpData(); + } + } else if (view.getId() == R.id.serverClickableLayout) { + HelperFunctions.showServerOptions(getContext(), ServersActivity.convertLocalServerData(currentServer), ServerLists.History); + } else if (view.getId() == R.id.appProtectionClickableLayout) { + Intent intent = new Intent(getContext(), AppsActivity.class); + intent.putExtra(AppsActivity.READ_ONLY_EXTRA, true); + getContext().startActivity(intent); + } else { + String textToCopy = currentServer.pk; + + ClipboardManager clipboard = (ClipboardManager)getContext().getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clipData = ClipData.newPlainText("", textToCopy); + clipboard.setPrimaryClip(clipData); + HelperFunctions.showToast(getContext().getString(R.string.general_copied), true); + } + } + + @Override + public void close() { + if (serverSubscription != null) { + serverSubscription.dispose(); + } + cancelIpCheck(); + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/start/connected/Chart.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/start/connected/Chart.java new file mode 100644 index 000000000..1c4489493 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/start/connected/Chart.java @@ -0,0 +1,158 @@ +package com.skywire.skycoin.vpn.activities.start.connected; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.widget.FrameLayout; +import android.widget.TextView; + +import com.github.mikephil.charting.charts.LineChart; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.data.LineData; +import com.github.mikephil.charting.data.LineDataSet; +import com.github.mikephil.charting.interfaces.datasets.ILineDataSet; +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.helpers.Globals; +import com.skywire.skycoin.vpn.helpers.HelperFunctions; +import com.skywire.skycoin.vpn.vpn.VPNGeneralPersistentData; + +import java.io.Closeable; +import java.util.ArrayList; + +import io.reactivex.rxjava3.disposables.Disposable; + +public class Chart extends FrameLayout implements Closeable { + public Chart(Context context) { + super(context); + Initialize(context, null); + } + public Chart(Context context, AttributeSet attrs) { + super(context, attrs); + Initialize(context, attrs); + } + public Chart(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + Initialize(context, attrs); + } + + private LineChart chart; + private FrameLayout chartContainer; + private TextView textMin; + private TextView textMid; + private TextView textMax; + + private Globals.DataUnits dataUnits = VPNGeneralPersistentData.getDataUnits(); + private ArrayList lastData; + private boolean showingMs; + + private Disposable dataUnitsSubscription; + + protected void Initialize (Context context, AttributeSet attrs) { + LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_start_chart, this, true); + + chart = findViewById(R.id.chart); + chartContainer = findViewById(R.id.chartContainer); + textMin = findViewById(R.id.textMin); + textMid = findViewById(R.id.textMid); + textMax = findViewById(R.id.textMax); + + chartContainer.setClipToOutline(true); + + chart.getDescription().setEnabled(false); + chart.getLegend().setEnabled(false); + chart.setDrawGridBackground(false); + chart.getXAxis().setEnabled(false); + chart.getAxisLeft().setEnabled(false); + chart.getAxisRight().setEnabled(false); + + chart.setViewPortOffsets(0f, 0f, 0f, 0f); + chart.getAxisLeft().setAxisMinimum(0); + chart.getAxisLeft().setSpaceTop(0); + chart.getAxisLeft().setSpaceBottom(0); + + chart.setScaleEnabled(false); + chart.setTouchEnabled(false); + + dataUnitsSubscription = VPNGeneralPersistentData.getDataUnitsObservable().subscribe(response -> { + dataUnits = response; + + if (lastData != null) { + setData(lastData, showingMs); + } + }); + } + + public void setData(ArrayList data, boolean showingMs) { + this.lastData = data; + this.showingMs = showingMs; + + ArrayList values = new ArrayList<>(); + + double max = 0; + for (int i = 0; i < data.size(); i++) { + double val = (float)data.get(i); + values.add(new Entry(i, (float)val)); + + if (val > max) { + max = val; + } + } + + if (max == 0) { + max = 1; + } + + double mid = max / 2; + + if (chart.getAxisLeft().getAxisMaximum() != max) { + chart.getAxisLeft().setAxisMaximum((float)max); + + if (showingMs) { + textMax.setText(HelperFunctions.getLatencyValue(max)); + textMid.setText(HelperFunctions.getLatencyValue(mid)); + textMin.setText(HelperFunctions.getLatencyValue(0)); + } else { + textMax.setText(HelperFunctions.computeDataAmountString(max, true, dataUnits != Globals.DataUnits.OnlyBytes)); + textMid.setText(HelperFunctions.computeDataAmountString(mid, true, dataUnits != Globals.DataUnits.OnlyBytes)); + textMin.setText(HelperFunctions.computeDataAmountString(0, true, dataUnits != Globals.DataUnits.OnlyBytes)); + } + } + + LineDataSet dataSet; + if (chart.getData() != null && chart.getData().getDataSetCount() > 0) { + dataSet = (LineDataSet) chart.getData().getDataSetByIndex(0); + dataSet.setValues(values); + dataSet.notifyDataSetChanged(); + chart.getData().notifyDataChanged(); + chart.notifyDataSetChanged(); + chart.invalidate(); + } else { + dataSet = new LineDataSet(values, ""); + dataSet.setDrawIcons(false); + dataSet.setDrawValues(false); + dataSet.setDrawCircleHole(false); + dataSet.setDrawCircles(false); + + dataSet.setMode(LineDataSet.Mode.HORIZONTAL_BEZIER); + + dataSet.setColor(0x59000000); + dataSet.setLineWidth(0f); + + dataSet.setDrawFilled(true); + dataSet.setFillColor(0x00000000); + dataSet.setFillAlpha(255); + + ArrayList dataSets = new ArrayList<>(); + dataSets.add(dataSet); + LineData lineData = new LineData(dataSets); + + chart.setData(lineData); + } + } + + @Override + public void close() { + dataUnitsSubscription.dispose(); + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/start/connected/StartViewConnected.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/start/connected/StartViewConnected.java new file mode 100644 index 000000000..d4129b09b --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/start/connected/StartViewConnected.java @@ -0,0 +1,509 @@ +package com.skywire.skycoin.vpn.activities.start.connected; + +import android.content.Context; +import android.content.Intent; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.activities.apps.AppsActivity; +import com.skywire.skycoin.vpn.activities.servers.ServerLists; +import com.skywire.skycoin.vpn.activities.servers.ServersActivity; +import com.skywire.skycoin.vpn.activities.start.StartViewRightPanel; +import com.skywire.skycoin.vpn.controls.ConfirmationModalWindow; +import com.skywire.skycoin.vpn.controls.ServerName; +import com.skywire.skycoin.vpn.extensible.ClickEvent; +import com.skywire.skycoin.vpn.helpers.ClickTimeManagement; +import com.skywire.skycoin.vpn.helpers.Globals; +import com.skywire.skycoin.vpn.helpers.HelperFunctions; +import com.skywire.skycoin.vpn.network.ApiClient; +import com.skywire.skycoin.vpn.vpn.VPNCoordinator; +import com.skywire.skycoin.vpn.vpn.VPNGeneralPersistentData; +import com.skywire.skycoin.vpn.vpn.VPNServersPersistentData; +import com.skywire.skycoin.vpn.vpn.VPNStates; + +import java.io.Closeable; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashSet; +import java.util.concurrent.TimeUnit; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public class StartViewConnected extends FrameLayout implements ClickEvent, Closeable { + public StartViewConnected(Context context) { + super(context); + Initialize(context, null); + } + public StartViewConnected(Context context, AttributeSet attrs) { + super(context, attrs); + Initialize(context, attrs); + } + public StartViewConnected(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + Initialize(context, attrs); + } + + private final int retryDelay = 20000; + + private TextView textTime; + private TextView textState; + private TextView textStateDescription; + private TextView textLastError; + private TextView textWaitingIp; + private TextView textWaitingCountry; + private TextView textIp; + private TextView textCountry; + private TextView textUploadSpeed; + private TextView textTotalUploaded; + private TextView textDownloadSpeed; + private TextView textTotalDownloaded; + private TextView textLatency; + private TextView textAppsProtectionMode; + private TextView textServerNote; + private TextView textStartedByTheSystem; + private ServerName serverName; + private ImageView imageStateLine; + private Chart downloadChart; + private Chart uploadChart; + private Chart latencyChart; + private LinearLayout leftContainer; + private LinearLayout ipDataContainer; + private LinearLayout ipContainer; + private LinearLayout countryContainer; + private FrameLayout appsContainer; + private LinearLayout appsInternalContainer; + private LinearLayout serverContainer; + private FrameLayout rightContainer; + private ProgressBar progressIp; + private ProgressBar progressCountry; + private StopButton buttonStop; + private StartViewRightPanel rightPanel; + + private String previousIp; + private String currentIp; + private String previousCountry; + private VPNCoordinator.ConnectionStats lastStats; + private boolean updateStats = true; + private Globals.DataUnits dataUnits = VPNGeneralPersistentData.getDataUnits(); + + private ClickTimeManagement appsButtonTimeManager = new ClickTimeManagement(); + private ClickTimeManagement serverButtonTimeManager = new ClickTimeManagement(); + + private Disposable serviceSubscription; + private Disposable serverSubscription; + private Disposable ipSubscription; + private Disposable statsSubscription; + private Disposable dataUnitsSubscription; + + protected void Initialize (Context context, AttributeSet attrs) { + LayoutInflater inflater = (LayoutInflater)context.getSystemService (Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_start_connected, this, true); + + textTime = findViewById(R.id.textTime); + textState = findViewById(R.id.textState); + textStateDescription = findViewById(R.id.textStateDescription); + textLastError = findViewById(R.id.textLastError); + textWaitingIp = findViewById(R.id.textWaitingIp); + textWaitingCountry = findViewById(R.id.textWaitingCountry); + textIp = findViewById(R.id.textIp); + textCountry = findViewById(R.id.textCountry); + textUploadSpeed = findViewById(R.id.textUploadSpeed); + textTotalUploaded = findViewById(R.id.textTotalUploaded); + textDownloadSpeed = findViewById(R.id.textDownloadSpeed); + textTotalDownloaded = findViewById(R.id.textTotalDownloaded); + textLatency = findViewById(R.id.textLatency); + textAppsProtectionMode = findViewById(R.id.textAppsProtectionMode); + textServerNote = findViewById(R.id.textServerNote); + textStartedByTheSystem = findViewById(R.id.textStartedByTheSystem); + serverName = this.findViewById (R.id.serverName); + imageStateLine = findViewById(R.id.imageStateLine); + imageStateLine = findViewById(R.id.imageStateLine); + downloadChart = findViewById(R.id.downloadChart); + uploadChart = findViewById(R.id.uploadChart); + latencyChart = findViewById(R.id.latencyChart); + leftContainer = findViewById(R.id.leftContainer); + ipDataContainer = findViewById(R.id.ipDataContainer); + ipContainer = findViewById(R.id.ipContainer); + countryContainer = findViewById(R.id.countryContainer); + appsContainer = findViewById(R.id.appsContainer); + appsInternalContainer = findViewById(R.id.appsInternalContainer); + serverContainer = findViewById(R.id.serverContainer); + rightContainer = findViewById(R.id.rightContainer); + progressIp = findViewById(R.id.progressIp); + progressCountry = findViewById(R.id.progressCountry); + buttonStop = findViewById(R.id.buttonStop); + rightPanel = findViewById(R.id.rightPanel); + + textLastError.setVisibility(GONE); + textStartedByTheSystem.setVisibility(GONE); + ipContainer.setVisibility(GONE); + countryContainer.setVisibility(GONE); + + if (HelperFunctions.getWidthType(getContext()) != HelperFunctions.WidthTypes.SMALL) { + float areaWidth = getContext().getResources().getDimension(R.dimen.tablet_status_area_width); + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams((int)Math.round(areaWidth), LayoutParams.WRAP_CONTENT); + params.gravity = Gravity.CENTER_HORIZONTAL; + leftContainer.setLayoutParams(params); + + ipDataContainer.setVisibility(GONE); + appsContainer.setVisibility(GONE); + serverContainer.setVisibility(GONE); + + textLastError.setTextSize(TypedValue.COMPLEX_UNIT_PX, getContext().getResources().getDimension(R.dimen.small_text_size)); + } else { + rightContainer.setVisibility(GONE); + } + + Globals.AppFilteringModes selectedMode = VPNGeneralPersistentData.getAppsSelectionMode(); + if (selectedMode != Globals.AppFilteringModes.PROTECT_ALL) { + HashSet selectedApps = HelperFunctions.filterAvailableApps(VPNGeneralPersistentData.getAppList(new HashSet<>())); + + if (HelperFunctions.getWidthType(getContext()) == HelperFunctions.WidthTypes.SMALL) { + if (selectedApps.size() > 0) { + if (selectedMode == Globals.AppFilteringModes.PROTECT_SELECTED) { + textAppsProtectionMode.setText(R.string.tmp_status_connected_protecting_selected_apps); + } else { + textAppsProtectionMode.setText(R.string.tmp_status_connected_ignoring_selected_apps); + } + + appsInternalContainer.setOnClickListener((View v) -> { + if (appsButtonTimeManager.canClick()) { + appsButtonTimeManager.informClickMade(); + Intent intent = new Intent(getContext(), AppsActivity.class); + intent.putExtra(AppsActivity.READ_ONLY_EXTRA, true); + getContext().startActivity(intent); + } + }); + } else { + appsContainer.setVisibility(GONE); + } + } else { + appsContainer.setVisibility(GONE); + } + } else { + appsContainer.setVisibility(GONE); + } + + if (!VPNGeneralPersistentData.getShowIpActivated()) { + textWaitingIp.setText(R.string.tmp_status_connected_ip_option_disabled); + textWaitingCountry.setText(R.string.tmp_status_connected_ip_option_disabled); + } + + ArrayList emptyValues = new ArrayList<>(); + emptyValues.add(0L); + + VPNCoordinator.ConnectionStats emptyStats = new VPNCoordinator.ConnectionStats(); + emptyStats.downloadSpeedHistory = emptyValues; + emptyStats.uploadSpeedHistory = emptyValues; + emptyStats.latencyHistory = emptyValues; + emptyStats.currentDownloadSpeed = 0; + emptyStats.currentUploadSpeed = 0; + emptyStats.currentLatency = 0; + emptyStats.totalDownloadedData = 0; + emptyStats.totalUploadedData = 0; + updateDisplayedStats(emptyStats); + + downloadChart.setData(emptyValues, false); + uploadChart.setData(emptyValues, false); + latencyChart.setData(emptyValues, true); + + serverSubscription = VPNServersPersistentData.getInstance().getCurrentServerObservable().subscribe(server -> { + serverName.setServer(ServersActivity.convertLocalServerData(server), ServerLists.History, true); + + String note = HelperFunctions.getServerNote(server); + if (note != null) { + textServerNote.setText(note); + } else { + textServerNote.setText(server.pk); + } + }); + + if (HelperFunctions.getWidthType(getContext()) == HelperFunctions.WidthTypes.SMALL) { + serverContainer.setOnClickListener((View v) -> { + if (serverButtonTimeManager.canClick()) { + serverButtonTimeManager.informClickMade(); + Observable.just(1).delay(Globals.CLICK_DELAY_MS, TimeUnit.MILLISECONDS) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(val -> { + HelperFunctions.showServerOptions( + getContext(), + ServersActivity.convertLocalServerData(VPNServersPersistentData.getInstance().getCurrentServer()), + ServerLists.History + ); + }); + } + }); + } + + buttonStop.setClickEventListener(this); + + serviceSubscription = VPNCoordinator.getInstance().getEventsObservable().subscribe( + state -> { + int mainText = VPNStates.getTitleForState(state.state); + if (mainText != -1) { + textState.setText(mainText); + } else { + textState.setText("---"); + } + + imageStateLine.setBackgroundResource(VPNStates.getColorForStateTitle(mainText)); + + int description = VPNStates.getDescriptionForState(state.state); + if (description != -1) { + textStateDescription.setText(description); + } else { + textStateDescription.setText("---"); + } + + buttonStop.setEnabled(true); + + if (state.startedByTheSystem) { + buttonStop.setEnabled(false); + textStartedByTheSystem.setVisibility(View.VISIBLE); + } else { + textStartedByTheSystem.setVisibility(View.GONE); + } + + if (state.stopRequested) { + buttonStop.setEnabled(false); + buttonStop.setBusyState(true); + } else { + buttonStop.setBusyState(false); + } + + if (state.state != VPNStates.CONNECTED) { + String lastError = VPNGeneralPersistentData.getLastError(null); + if (lastError != null) { + String start = getContext().getString(R.string.tmp_status_page_last_error); + textLastError.setText(start + " " + lastError); + textLastError.setVisibility(VISIBLE); + } else { + textLastError.setVisibility(GONE); + } + } else { + textLastError.setVisibility(GONE); + } + + if (VPNGeneralPersistentData.getShowIpActivated()) { + if (HelperFunctions.getWidthType(getContext()) == HelperFunctions.WidthTypes.SMALL) { + if (state.state == VPNStates.CONNECTED) { + if (ipContainer.getVisibility() == TextView.GONE) { + ipContainer.setVisibility(VISIBLE); + countryContainer.setVisibility(VISIBLE); + textWaitingIp.setVisibility(GONE); + textWaitingCountry.setVisibility(GONE); + + textIp.setText("---"); + textCountry.setText("---"); + + getIp(0); + } + } else { + if (ipContainer.getVisibility() == TextView.VISIBLE) { + ipContainer.setVisibility(GONE); + countryContainer.setVisibility(GONE); + textWaitingIp.setVisibility(VISIBLE); + textWaitingCountry.setVisibility(VISIBLE); + + cancelIpCheck(); + } + } + } else { + if (state.state == VPNStates.CONNECTED) { + rightPanel.refreshIpData(); + } else { + rightPanel.putInWaitingForVpnState(); + } + } + } + } + ); + + statsSubscription = VPNCoordinator.getInstance().getConnectionStats().subscribe(stats -> { + lastStats = stats; + if (updateStats) { + updateDisplayedStats(lastStats); + } + }); + + dataUnitsSubscription = VPNGeneralPersistentData.getDataUnitsObservable().subscribe(response -> { + dataUnits = response; + + if (lastStats != null && updateStats) { + updateDisplayedStats(lastStats); + } + }); + + updateTime(null); + } + + private void updateDisplayedStats(VPNCoordinator.ConnectionStats stats) { + if (stats != null) { + updateTime(stats.lastConnectionDate); + + downloadChart.setData(stats.downloadSpeedHistory, false); + uploadChart.setData(stats.uploadSpeedHistory, false); + latencyChart.setData(stats.latencyHistory, true); + + textDownloadSpeed.setText(HelperFunctions.computeDataAmountString(stats.currentDownloadSpeed, true, dataUnits != Globals.DataUnits.OnlyBytes)); + textUploadSpeed.setText(HelperFunctions.computeDataAmountString(stats.currentUploadSpeed, true, dataUnits != Globals.DataUnits.OnlyBytes)); + textLatency.setText(HelperFunctions.getLatencyValue(stats.currentLatency)); + + textTotalDownloaded.setText(String.format( + getContext().getText(R.string.tmp_status_connected_total_data).toString(), + HelperFunctions.computeDataAmountString(stats.totalDownloadedData, false, dataUnits == Globals.DataUnits.OnlyBits) + )); + + textTotalUploaded.setText(String.format( + getContext().getText(R.string.tmp_status_connected_total_data).toString(), + HelperFunctions.computeDataAmountString(stats.totalUploadedData, false, dataUnits == Globals.DataUnits.OnlyBits) + )); + } + } + + public void pauseUpdatingStats() { + updateStats = false; + } + + public void continueUpdatingStats() { + updateStats = true; + updateDisplayedStats(lastStats); + } + + public void updateRightBar() { + rightPanel.updateData(); + } + + private void updateTime(Date lastConnectionDate) { + if (lastConnectionDate == null) { + textTime.setText(R.string.tmp_status_connected_waiting); + } else { + long connectionMs = (new Date()).getTime() - lastConnectionDate.getTime(); + + String time = String.format("%02d", connectionMs / 3600000) + ":"; + time += String.format("%02d", (connectionMs / 60000) % 60) + ":"; + time += String.format("%02d", (connectionMs / 1000) % 60); + + textTime.setText(time); + } + } + + private void getIp(int delayMs) { + if (!VPNGeneralPersistentData.getShowIpActivated()) { + return; + } + + if (ipSubscription != null) { + ipSubscription.dispose(); + } + + progressIp.setVisibility(VISIBLE); + progressCountry.setVisibility(VISIBLE); + + this.ipSubscription = Observable.just(0).delay(delayMs, TimeUnit.MILLISECONDS).flatMap(v -> ApiClient.getCurrentIp()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(response -> { + if (response.body() != null) { + progressIp.setVisibility(GONE); + + currentIp = response.body().ip; + textIp.setText(currentIp); + + if (currentIp.equals(previousIp) && previousCountry != null) { + textCountry.setText(previousCountry); + progressCountry.setVisibility(GONE); + } else { + getIpCountry(0); + } + + previousIp = currentIp; + } else { + getIp(retryDelay); + } + }, err -> { + getIp(retryDelay); + }); + } + + private void getIpCountry(int delayMs) { + if (!VPNGeneralPersistentData.getShowIpActivated()) { + return; + } + + ipSubscription.dispose(); + + this.ipSubscription = Observable.just(0).delay(delayMs, TimeUnit.MILLISECONDS).flatMap(v -> ApiClient.getIpCountry(currentIp)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(response -> { + if (response.body() != null) { + progressCountry.setVisibility(GONE); + + String[] dataParts = response.body().split(";"); + if (dataParts.length == 4) { + textCountry.setText(dataParts[3]); + } else { + textCountry.setText(getContext().getText(R.string.general_unknown)); + } + + previousCountry = textCountry.getText().toString(); + } else { + getIpCountry(retryDelay); + } + }, err -> { + getIpCountry(retryDelay); + }); + } + + @Override + public void close() { + serverSubscription.dispose(); + serviceSubscription.dispose(); + statsSubscription.dispose(); + dataUnitsSubscription.dispose(); + rightPanel.close(); + downloadChart.close(); + uploadChart.close(); + latencyChart.close(); + cancelIpCheck(); + } + + private void cancelIpCheck() { + if (ipSubscription != null) { + ipSubscription.dispose(); + } + } + + @Override + public void onClick(View view) { + if (!VPNGeneralPersistentData.getKillSwitchActivated()) { + VPNCoordinator.getInstance().stopVPN(); + } else { + ConfirmationModalWindow confirmationModal = new ConfirmationModalWindow( + getContext(), + R.string.tmp_status_connected_disconnect_confirmation, + R.string.tmp_confirmation_yes, + R.string.tmp_confirmation_no, + () -> { + VPNCoordinator.getInstance().stopVPN(); + buttonStop.setEnabled(false); + } + ); + confirmationModal.show(); + } + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/start/connected/StopButton.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/start/connected/StopButton.java new file mode 100644 index 000000000..b956b2fd7 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/start/connected/StopButton.java @@ -0,0 +1,89 @@ +package com.skywire.skycoin.vpn.activities.start.connected; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ProgressBar; +import android.widget.TextView; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.extensible.ButtonBase; + +public class StopButton extends ButtonBase implements View.OnTouchListener { + public StopButton(Context context) { + super(context); + } + public StopButton(Context context, AttributeSet attrs) { + super(context, attrs); + } + public StopButton(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + private FrameLayout mainLayout; + private FrameLayout internalContainer; + private TextView textIcon; + private ProgressBar progressAnimation; + + protected void Initialize (Context context, AttributeSet attrs) { + LayoutInflater inflater = (LayoutInflater)context.getSystemService (Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_stop_button, this, true); + + mainLayout = this.findViewById(R.id.mainLayout); + internalContainer = this.findViewById(R.id.internalContainer); + textIcon = this.findViewById(R.id.textIcon); + progressAnimation = this.findViewById(R.id.progressAnimation); + + progressAnimation.setVisibility(GONE); + + internalContainer.setClipToOutline(true); + + setOnTouchListener(this); + setViewForCheckingClicks(this); + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + mainLayout.setScaleX(0.98f); + mainLayout.setScaleY(0.98f); + } else if (event.getAction() == MotionEvent.ACTION_CANCEL || event.getAction() == MotionEvent.ACTION_POINTER_UP || event.getAction() == MotionEvent.ACTION_UP) { + mainLayout.setScaleX(1.0f); + mainLayout.setScaleY(1.0f); + } + + return false; + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + + if (enabled) { + setAlpha(1f); + } else { + setAlpha(0.5f); + } + } + + public void setBusyState(boolean busy) { + if (busy) { + if (!getBusyState()) { + progressAnimation.setVisibility(VISIBLE); + textIcon.setVisibility(GONE); + } + } else { + if (getBusyState()) { + progressAnimation.setVisibility(GONE); + textIcon.setVisibility(VISIBLE); + } + } + } + + public boolean getBusyState() { + return progressAnimation.getVisibility() == VISIBLE; + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/start/disconnected/CurrentServerButton.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/start/disconnected/CurrentServerButton.java new file mode 100644 index 000000000..af847828d --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/start/disconnected/CurrentServerButton.java @@ -0,0 +1,89 @@ +package com.skywire.skycoin.vpn.activities.start.disconnected; + +import android.content.Context; +import android.graphics.drawable.RippleDrawable; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.activities.servers.ServerLists; +import com.skywire.skycoin.vpn.activities.servers.ServersActivity; +import com.skywire.skycoin.vpn.controls.ServerName; +import com.skywire.skycoin.vpn.extensible.ButtonBase; +import com.skywire.skycoin.vpn.helpers.HelperFunctions; +import com.skywire.skycoin.vpn.objects.LocalServerData; + +public class CurrentServerButton extends ButtonBase implements View.OnTouchListener { + public CurrentServerButton(Context context) { + super(context); + } + public CurrentServerButton(Context context, AttributeSet attrs) { + super(context, attrs); + } + public CurrentServerButton(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + private FrameLayout mainContainer; + private FrameLayout internalContainer; + private LinearLayout serverContainer; + private ImageView imageFlag; + private ServerName serverName; + private TextView textBottom; + private TextView textNoServer; + + private RippleDrawable rippleDrawable; + + @Override + protected void Initialize (Context context, AttributeSet attrs) { + LayoutInflater inflater = (LayoutInflater)context.getSystemService (Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_current_server_button, this, true); + + mainContainer = this.findViewById (R.id.mainContainer); + internalContainer = this.findViewById (R.id.internalContainer); + serverContainer = this.findViewById (R.id.serverContainer); + imageFlag = this.findViewById (R.id.imageFlag); + serverName = this.findViewById (R.id.serverName); + textBottom = this.findViewById (R.id.textBottom); + textNoServer = this.findViewById (R.id.textNoServer); + + rippleDrawable = (RippleDrawable) internalContainer.getBackground(); + + mainContainer.setClipToOutline(true); + imageFlag.setClipToOutline(true); + + setOnTouchListener(this); + setViewForCheckingClicks(this); + } + + public void setData (LocalServerData currentServer) { + if (currentServer == null || currentServer.pk == null) { + textNoServer.setVisibility(VISIBLE); + serverContainer.setVisibility(GONE); + + return; + } + + serverContainer.setVisibility(VISIBLE); + textNoServer.setVisibility(GONE); + + serverName.setServer(ServersActivity.convertLocalServerData(currentServer), ServerLists.History, true); + textBottom.setText(currentServer.pk); + imageFlag.setImageResource(HelperFunctions.getFlagResourceId(currentServer.countryCode)); + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (rippleDrawable != null) { + rippleDrawable.setHotspot(event.getX(), event.getY()); + } + + return false; + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/start/disconnected/StartButton.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/start/disconnected/StartButton.java new file mode 100644 index 000000000..7ba05604e --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/start/disconnected/StartButton.java @@ -0,0 +1,95 @@ +package com.skywire.skycoin.vpn.activities.start.disconnected; + +import android.animation.Animator; +import android.animation.AnimatorInflater; +import android.animation.AnimatorSet; +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.extensible.ButtonBase; + +public class StartButton extends ButtonBase implements Animator.AnimatorListener, View.OnTouchListener { + public StartButton(Context context) { + super(context); + } + public StartButton(Context context, AttributeSet attrs) { + super(context, attrs); + } + public StartButton(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + private FrameLayout mainLayout; + private ImageView imageAnim; + private ImageView imageBackground; + + private AnimatorSet animSet; + + protected void Initialize (Context context, AttributeSet attrs) { + LayoutInflater inflater = (LayoutInflater)context.getSystemService (Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_start_button, this, true); + + mainLayout = this.findViewById(R.id.mainLayout); + imageAnim = this.findViewById(R.id.imageAnim); + imageBackground = this.findViewById(R.id.imageBackground); + + animSet = (AnimatorSet) AnimatorInflater.loadAnimator(getContext(), R.animator.anim_start_button); + animSet.setTarget(imageAnim); + + setOnTouchListener(this); + setViewForCheckingClicks(this); + } + + public void startAnimation() { + animSet.addListener(this); + animSet.start(); + } + + public void stopAnimation() { + animSet.removeAllListeners(); + animSet.cancel(); + } + + @Override + public void onAnimationStart(Animator animation) { } + @Override + public void onAnimationCancel(Animator animation) { } + @Override + public void onAnimationRepeat(Animator animation) { } + @Override + public void onAnimationEnd(Animator animation) { + animSet.start(); + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + mainLayout.setScaleX(0.9f); + mainLayout.setScaleY(0.9f); + imageBackground.setAlpha(1.0f); + } else if (event.getAction() == MotionEvent.ACTION_CANCEL || event.getAction() == MotionEvent.ACTION_POINTER_UP || event.getAction() == MotionEvent.ACTION_UP) { + mainLayout.setScaleX(1.0f); + mainLayout.setScaleY(1.0f); + imageBackground.setAlpha(0.7f); + } + + return false; + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + + if (enabled) { + setAlpha(1f); + } else { + setAlpha(0.5f); + } + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/start/disconnected/StartViewDisconnected.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/start/disconnected/StartViewDisconnected.java new file mode 100644 index 000000000..67d59299e --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/activities/start/disconnected/StartViewDisconnected.java @@ -0,0 +1,156 @@ +package com.skywire.skycoin.vpn.activities.start.disconnected; + +import android.app.Activity; +import android.content.Context; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.TextView; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.activities.index.IndexPageAdapter; +import com.skywire.skycoin.vpn.activities.servers.ServerLists; +import com.skywire.skycoin.vpn.activities.servers.ServersActivity; +import com.skywire.skycoin.vpn.activities.start.StartViewRightPanel; +import com.skywire.skycoin.vpn.extensible.ClickEvent; +import com.skywire.skycoin.vpn.helpers.HelperFunctions; +import com.skywire.skycoin.vpn.objects.LocalServerData; +import com.skywire.skycoin.vpn.vpn.VPNGeneralPersistentData; +import com.skywire.skycoin.vpn.vpn.VPNServersPersistentData; + +import java.io.Closeable; + +import io.reactivex.rxjava3.disposables.Disposable; + +public class StartViewDisconnected extends FrameLayout implements ClickEvent, Closeable { + public StartViewDisconnected(Context context) { + super(context); + Initialize(context, null); + } + public StartViewDisconnected(Context context, AttributeSet attrs) { + super(context, attrs); + Initialize(context, attrs); + } + public StartViewDisconnected(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + Initialize(context, attrs); + } + + private CurrentServerButton viewCurrentServerButton; + private StartButton startButton; + private TextView textServerNote; + private TextView textLastError; + private FrameLayout rightContainer; + private StartViewRightPanel rightPanel; + + private Activity parentActivity; + private IndexPageAdapter.RequestTabListener requestTabListener; + private Disposable currentServerSubscription; + + protected void Initialize (Context context, AttributeSet attrs) { + LayoutInflater inflater = (LayoutInflater)context.getSystemService (Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_start_disconnected, this, true); + + viewCurrentServerButton = findViewById(R.id.viewCurrentServerButton); + startButton = findViewById(R.id.startButton); + textServerNote = findViewById(R.id.textServerNote); + textLastError = findViewById(R.id.textLastError); + rightContainer = findViewById(R.id.rightContainer); + rightPanel = findViewById(R.id.rightPanel); + + viewCurrentServerButton.setClickEventListener(this); + startButton.setClickEventListener(this); + + currentServerSubscription = VPNServersPersistentData.getInstance().getCurrentServerObservable().subscribe(currentServer -> { + viewCurrentServerButton.setData(currentServer); + updateNote(currentServer); + }); + + setErrorMsg(VPNGeneralPersistentData.getLastError(null)); + + if (HelperFunctions.getWidthType(getContext()) == HelperFunctions.WidthTypes.SMALL) { + rightContainer.setVisibility(GONE); + } else { + textServerNote.setTextSize(TypedValue.COMPLEX_UNIT_PX, getContext().getResources().getDimension(R.dimen.small_text_size)); + textLastError.setTextSize(TypedValue.COMPLEX_UNIT_PX, getContext().getResources().getDimension(R.dimen.small_text_size)); + rightPanel.refreshIpData(); + } + } + + public void setRequestTabListener(IndexPageAdapter.RequestTabListener listener) { + requestTabListener = listener; + } + + public void setParentActivity(Activity activity) { + parentActivity = activity; + } + + public void startAnimation() { + startButton.startAnimation(); + } + + public void stopAnimation() { + startButton.stopAnimation(); + } + + public void updateRightBar() { + rightPanel.updateData(); + } + + public void setErrorMsg(String errorMsg) { + if (errorMsg != null) { + String start = getContext().getString(R.string.tmp_status_page_last_error); + textLastError.setText(start + " " + errorMsg); + textLastError.setVisibility(VISIBLE); + } else { + textLastError.setVisibility(GONE); + } + } + + private void updateNote(LocalServerData currentServer) { + if (currentServer == null) { + textServerNote.setVisibility(GONE); + + return; + } + + String note = HelperFunctions.getServerNote(currentServer); + + if (note != null) { + textServerNote.setText(note); + textServerNote.setVisibility(VISIBLE); + } else { + textServerNote.setVisibility(GONE); + } + } + + @Override + public void close() { + currentServerSubscription.dispose(); + rightPanel.close(); + stopAnimation(); + } + + @Override + public void onClick(View view) { + LocalServerData currentServer = VPNServersPersistentData.getInstance().getCurrentServer(); + if (currentServer != null) { + if (view.getId() == R.id.viewCurrentServerButton) { + HelperFunctions.showServerOptions(getContext(), ServersActivity.convertLocalServerData(currentServer), ServerLists.History); + } else { + if (parentActivity != null) { + boolean starting = HelperFunctions.prepareAndStartVpn(parentActivity, currentServer); + if (starting) { + startButton.setEnabled(false); + } + } + } + } else { + if (requestTabListener != null) { + requestTabListener.onOpenServerListRequested(); + } + } + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/BoxRowBackground.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/BoxRowBackground.java new file mode 100644 index 000000000..9df7c76f0 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/BoxRowBackground.java @@ -0,0 +1,66 @@ +package com.skywire.skycoin.vpn.controls; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.Shader; +import android.graphics.drawable.BitmapDrawable; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewOutlineProvider; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.helpers.BoxRowTypes; + +public class BoxRowBackground extends View { + public BoxRowBackground(Context context) { + super(context); + Initialize(context, null); + } + public BoxRowBackground(Context context, AttributeSet attrs) { + super(context, attrs); + Initialize(context, attrs); + } + public BoxRowBackground(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + Initialize(context, attrs); + } + + BitmapDrawable bitmapDrawable; + + private void Initialize (Context context, AttributeSet attrs) { + setOutlineProvider(ViewOutlineProvider.BACKGROUND); + setClipToOutline(true); + + Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.box_pattern); + + bitmapDrawable = new BitmapDrawable(context.getResources(), bitmap); + bitmapDrawable.setTileModeXY(Shader.TileMode.REPEAT, Shader.TileMode.REPEAT); + + setType(BoxRowTypes.TOP); + } + + @Override + protected void onDraw(Canvas canvas) { + bitmapDrawable.setBounds(new Rect(0, 0, canvas.getWidth(), canvas.getHeight())); + bitmapDrawable.draw(canvas); + + super.onDraw(canvas); + } + + public void setType(BoxRowTypes type) { + if (type == BoxRowTypes.TOP) { + setBackgroundResource(R.drawable.box_row_rounded_box_1); + } else if (type == BoxRowTypes.MIDDLE) { + setBackgroundResource(R.drawable.box_row_rounded_box_2); + } else if (type == BoxRowTypes.BOTTOM) { + setBackgroundResource(R.drawable.box_row_rounded_box_3); + } else { + setBackgroundResource(R.drawable.box_row_rounded_box_4); + } + + this.invalidate(); + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/BoxRowLayout.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/BoxRowLayout.java new file mode 100644 index 000000000..868c713d2 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/BoxRowLayout.java @@ -0,0 +1,219 @@ +package com.skywire.skycoin.vpn.controls; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; +import android.widget.FrameLayout; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.extensible.ClickEvent; +import com.skywire.skycoin.vpn.helpers.BoxRowTypes; +import com.skywire.skycoin.vpn.helpers.HelperFunctions; + +public class BoxRowLayout extends FrameLayout implements ClickEvent { + public BoxRowLayout(Context context) { + super(context); + Initialize(context, null); + } + public BoxRowLayout(Context context, AttributeSet attrs) { + super(context, attrs); + Initialize(context, attrs); + } + public BoxRowLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + Initialize(context, attrs); + } + + private View baseBackground; + private BoxRowBackground background; + private BoxRowRipple ripple; + private View separator; + + private ClickEvent clickListener; + + private boolean addExtraPaddingForTablets = false; + private boolean ignoreMargins = false; + private boolean ignoreClicks = false; + private boolean hideSeparator = false; + + private int tabletExtraHorizontalPadding = 0; + private float horizontalPadding; + private float verticalPadding; + + private void Initialize (Context context, AttributeSet attrs) { + baseBackground = new View(context); + background = new BoxRowBackground(context); + ripple = new BoxRowRipple(context); + separator = new View(context); + + int type = 1; + + if (attrs != null) { + TypedArray attributes = context.getTheme().obtainStyledAttributes( + attrs, + R.styleable.BoxRowLayout, + 0, 0 + ); + + type = attributes.getInteger(R.styleable.BoxRowLayout_box_row_type, 1); + + addExtraPaddingForTablets = attributes.getBoolean(R.styleable.BoxRowLayout_add_extra_padding_for_tablets, false); + ignoreMargins = attributes.getBoolean(R.styleable.BoxRowLayout_ignore_margins, false); + ignoreClicks = attributes.getBoolean(R.styleable.BoxRowLayout_ignore_clicks, false); + hideSeparator = attributes.getBoolean(R.styleable.BoxRowLayout_hide_separator, false); + + setUseBigFastClickPrevention(attributes.getBoolean(R.styleable.BoxRowLayout_use_big_fast_click_prevention, true)); + + attributes.recycle(); + } + + horizontalPadding = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 10, + getResources().getDisplayMetrics() + ); + if (!ignoreMargins) { + horizontalPadding += getContext().getResources().getDimension(R.dimen.box_row_layout_horizontal_padding); + } + + verticalPadding = 0; + if (!ignoreMargins) { + verticalPadding += getContext().getResources().getDimension(R.dimen.box_row_layout_vertical_padding); + } + + if (addExtraPaddingForTablets) { + tabletExtraHorizontalPadding = HelperFunctions.getTabletExtraHorizontalPadding(getContext()); + } + + separator.setBackgroundResource(R.color.box_separator); + + if (type == 0) { + setType(BoxRowTypes.TOP); + } else if (type == 1) { + setType(BoxRowTypes.MIDDLE); + } else if (type == 2) { + setType(BoxRowTypes.BOTTOM); + } else if (type == 3) { + setType(BoxRowTypes.SINGLE); + } + + this.setClipToPadding(false); + + this.addView(baseBackground); + this.addView(background); + if (!ignoreClicks) { + ripple.setClickEventListener(this); + this.addView(ripple); + } + this.addView(separator); + + setClickable(false); + } + + public void setClickEventListener(ClickEvent listener) { + clickListener = listener; + } + + public void setUseBigFastClickPrevention(boolean useBigFastClickPrevention) { + ripple.setUseBigFastClickPrevention(useBigFastClickPrevention); + } + + public void setType(BoxRowTypes type) { + float bottomPaddingExtra = 0; + float topPaddingExtra = 0; + + if (type == BoxRowTypes.TOP) { + baseBackground.setBackgroundResource(R.drawable.background_box1); + + topPaddingExtra = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 10, + getResources().getDisplayMetrics() + ); + + separator.setVisibility(View.VISIBLE); + } else if (type == BoxRowTypes.MIDDLE) { + baseBackground.setBackgroundResource(R.drawable.background_box2); + separator.setVisibility(View.VISIBLE); + } else if (type == BoxRowTypes.BOTTOM) { + baseBackground.setBackgroundResource(R.drawable.background_box3); + + bottomPaddingExtra = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 15, + getResources().getDisplayMetrics() + ); + + separator.setVisibility(View.GONE); + } else if (type == BoxRowTypes.SINGLE) { + baseBackground.setBackgroundResource(R.drawable.background_box4); + + topPaddingExtra = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 10, + getResources().getDisplayMetrics() + ); + bottomPaddingExtra = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 15, + getResources().getDisplayMetrics() + ); + + separator.setVisibility(View.GONE); + } + + if (hideSeparator) { + separator.setVisibility(View.GONE); + } + + int finalLeftPadding = (int)horizontalPadding; + int finalTopPadding = (int)(verticalPadding + topPaddingExtra); + int finalRightPadding = (int)horizontalPadding; + int finalBottomPadding = (int)(verticalPadding + bottomPaddingExtra); + + this.setPadding(finalLeftPadding + tabletExtraHorizontalPadding, finalTopPadding, finalRightPadding + tabletExtraHorizontalPadding, finalBottomPadding); + + FrameLayout.LayoutParams backgroundLayoutParams = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + backgroundLayoutParams.leftMargin = -finalLeftPadding; + backgroundLayoutParams.rightMargin = -finalRightPadding; + if (finalTopPadding > 0) { + backgroundLayoutParams.topMargin = -finalTopPadding; + } + if (finalBottomPadding > 0) { + backgroundLayoutParams.bottomMargin = -finalBottomPadding; + } + + baseBackground.setLayoutParams(backgroundLayoutParams); + background.setLayoutParams(backgroundLayoutParams); + background.setType(type); + if (!ignoreClicks) { + ripple.setLayoutParams(backgroundLayoutParams); + ripple.setType(type); + } + + float separatorHeight = getContext().getResources().getDimension(R.dimen.box_row_layout_separator_height); + float separatorHorizontalMargin; + if (ignoreMargins) { + separatorHorizontalMargin = getContext().getResources().getDimension(R.dimen.box_row_layout_separator_combined_horizontal_margin); + } else { + separatorHorizontalMargin = getContext().getResources().getDimension(R.dimen.box_row_layout_separator_horizontal_margin); + } + + FrameLayout.LayoutParams separatorLayoutParams = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, (int)Math.round(separatorHeight)); + separatorLayoutParams.gravity = Gravity.BOTTOM; + separatorLayoutParams.bottomMargin = -finalBottomPadding; + separatorLayoutParams.leftMargin = (int)separatorHorizontalMargin; + separatorLayoutParams.rightMargin = (int)separatorHorizontalMargin; + separator.setLayoutParams(separatorLayoutParams); + } + + @Override + public void onClick(View view) { + if (clickListener != null) { + clickListener.onClick(this); + } + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/BoxRowRipple.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/BoxRowRipple.java new file mode 100644 index 000000000..42ea08585 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/BoxRowRipple.java @@ -0,0 +1,68 @@ +package com.skywire.skycoin.vpn.controls; + +import android.content.Context; +import android.graphics.drawable.RippleDrawable; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewOutlineProvider; +import android.widget.FrameLayout; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.extensible.ButtonBase; +import com.skywire.skycoin.vpn.helpers.BoxRowTypes; + +public class BoxRowRipple extends ButtonBase implements View.OnTouchListener { + public BoxRowRipple(Context context) { + super(context); + } + public BoxRowRipple(Context context, AttributeSet attrs) { + super(context, attrs); + } + public BoxRowRipple(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + RippleDrawable rippleDrawable; + + @Override + protected void Initialize (Context context, AttributeSet attrs) { + setOutlineProvider(ViewOutlineProvider.BACKGROUND); + setClipToOutline(true); + setClickable(true); + + View ripple = new View(context); + FrameLayout.LayoutParams rippleLayoutParams = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + ripple.setLayoutParams(rippleLayoutParams); + ripple.setBackgroundResource(R.drawable.box_ripple); + this.addView(ripple); + + rippleDrawable = (RippleDrawable) ripple.getBackground(); + + ripple.setOnTouchListener(this); + setViewForCheckingClicks(ripple); + + setType(BoxRowTypes.TOP); + } + + public void setType(BoxRowTypes type) { + if (type == BoxRowTypes.TOP) { + setBackgroundResource(R.drawable.box_row_rounded_box_1); + } else if (type == BoxRowTypes.MIDDLE) { + setBackgroundResource(R.drawable.box_row_rounded_box_2); + } else if (type == BoxRowTypes.BOTTOM) { + setBackgroundResource(R.drawable.box_row_rounded_box_3); + } else { + setBackgroundResource(R.drawable.box_row_rounded_box_4); + } + + this.invalidate(); + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + rippleDrawable.setHotspot(event.getX(), event.getY()); + + return false; + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/ClickableLinearLayout.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/ClickableLinearLayout.java new file mode 100644 index 000000000..78939294e --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/ClickableLinearLayout.java @@ -0,0 +1,66 @@ +package com.skywire.skycoin.vpn.controls; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.widget.LinearLayout; + +import com.skywire.skycoin.vpn.extensible.ClickEvent; +import com.skywire.skycoin.vpn.helpers.ClickTimeManagement; +import com.skywire.skycoin.vpn.helpers.Globals; + +import java.util.concurrent.TimeUnit; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public class ClickableLinearLayout extends LinearLayout implements View.OnTouchListener, View.OnClickListener { + private ClickEvent clickListener; + private ClickTimeManagement buttonTimeManager = new ClickTimeManagement(); + + public ClickableLinearLayout(Context context) { + super(context); + Initialize(context, null); + } + public ClickableLinearLayout(Context context, AttributeSet attrs) { + super(context, attrs); + Initialize(context, attrs); + } + public ClickableLinearLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + Initialize(context, attrs); + } + + protected void Initialize (Context context, AttributeSet attrs) { + setOnTouchListener(this); + setOnClickListener(this); + } + + public void setClickEventListener(ClickEvent listener) { + clickListener = listener; + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + setAlpha(0.5f); + } else if (event.getAction() == MotionEvent.ACTION_CANCEL || event.getAction() == MotionEvent.ACTION_POINTER_UP || event.getAction() == MotionEvent.ACTION_UP) { + setAlpha(1f); + } + + return false; + } + + @Override + public void onClick(View view) { + if (clickListener != null && buttonTimeManager.canClick()) { + buttonTimeManager.informClickMade(); + Observable.just(1).delay(Globals.CLICK_DELAY_MS, TimeUnit.MILLISECONDS) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(v -> clickListener.onClick(this)); + } + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/ConfirmationModalWindow.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/ConfirmationModalWindow.java new file mode 100644 index 000000000..cea138f40 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/ConfirmationModalWindow.java @@ -0,0 +1,65 @@ +package com.skywire.skycoin.vpn.controls; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.view.View; +import android.view.Window; +import android.widget.TextView; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.extensible.ClickEvent; +import com.skywire.skycoin.vpn.helpers.HelperFunctions; + +public class ConfirmationModalWindow extends Dialog implements ClickEvent { + public interface Confirmed { + void confirmed(); + } + + private TextView text; + private ModalWindowButton buttonCancel; + private ModalWindowButton buttonConfirm; + + private int textResource; + private int confirmBtnResource; + private int cancelBtnResource; + private Confirmed event; + + public ConfirmationModalWindow(Context ctx, int textResource, int confirmBtnResource, int cancelBtnResource, Confirmed event) { + super(ctx); + + this.textResource = textResource; + this.confirmBtnResource = confirmBtnResource; + this.cancelBtnResource = cancelBtnResource; + this.event = event; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_NO_TITLE); + setContentView(R.layout.view_confirmation_dialog); + + text = findViewById(R.id.text); + buttonCancel = findViewById(R.id.buttonCancel); + buttonConfirm = findViewById(R.id.buttonConfirm); + + text.setText(textResource); + buttonCancel.setText(cancelBtnResource); + buttonConfirm.setText(confirmBtnResource); + + buttonCancel.setClickEventListener(this); + buttonConfirm.setClickEventListener(this); + + HelperFunctions.configureModalWindow(this); + } + + @Override + public void onClick(View view) { + if (view.getId() == R.id.buttonConfirm && event != null) { + event.confirmed(); + } + + dismiss(); + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/EditServerValueModalWindow.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/EditServerValueModalWindow.java new file mode 100644 index 000000000..e11fa85e9 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/EditServerValueModalWindow.java @@ -0,0 +1,122 @@ +package com.skywire.skycoin.vpn.controls; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.View; +import android.view.Window; +import android.view.inputmethod.EditorInfo; +import android.widget.EditText; + +import com.google.android.material.textfield.TextInputLayout; +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.activities.servers.VpnServerForList; +import com.skywire.skycoin.vpn.extensible.ClickEvent; +import com.skywire.skycoin.vpn.helpers.HelperFunctions; +import com.skywire.skycoin.vpn.objects.LocalServerData; +import com.skywire.skycoin.vpn.vpn.VPNServersPersistentData; + +public class EditServerValueModalWindow extends Dialog implements ClickEvent { + private ModalBase modalBase; + private TextInputLayout editContainer; + private EditText editValue; + private ModalWindowButton buttonCancel; + private ModalWindowButton buttonConfirm; + + private boolean editingName; + private VpnServerForList server; + + public EditServerValueModalWindow(Context ctx, boolean editingName, VpnServerForList server) { + super(ctx); + + this.editingName = editingName; + this.server = server; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_NO_TITLE); + setContentView(R.layout.view_edit_server_value_modal); + + modalBase = findViewById(R.id.modalBase); + editContainer = findViewById(R.id.editContainer); + editValue = findViewById(R.id.editValue); + buttonCancel = findViewById(R.id.buttonCancel); + buttonConfirm = findViewById(R.id.buttonConfirm); + + LocalServerData localServerData = VPNServersPersistentData.getInstance().processFromList(server); + if (editingName) { + modalBase.setTitle(R.string.tmp_edit_value_name_title); + editContainer.setHint(getContext().getText(R.string.tmp_edit_value_name_label)); + + if (localServerData.customName != null) { + editValue.setText(localServerData.customName); + } else { + editValue.setText(""); + } + } else { + modalBase.setTitle(R.string.tmp_edit_value_note_title); + editContainer.setHint(getContext().getText(R.string.tmp_edit_value_note_label)); + + if (localServerData.personalNote != null) { + editValue.setText(localServerData.personalNote); + } else { + editValue.setText(""); + } + } + + editValue.setOnEditorActionListener((v, actionId, event) -> { + if ( + actionId == EditorInfo.IME_ACTION_DONE || + (event != null && event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_ENTER) + ) { + makeChange(); + dismiss(); + + return true; + } + + return false; + }); + + editValue.setSelection(editValue.getText().length()); + + buttonCancel.setClickEventListener(this); + buttonConfirm.setClickEventListener(this); + + HelperFunctions.configureModalWindow(this); + } + + @Override + public void onClick(View view) { + if (view.getId() == R.id.buttonConfirm) { + makeChange(); + } + + dismiss(); + } + + private void makeChange() { + LocalServerData localServerData = VPNServersPersistentData.getInstance().processFromList(server); + + String newValue = editValue.getText().toString().trim(); + String currentValue = editingName ? localServerData.customName : localServerData.personalNote; + if (currentValue == null) { + currentValue = ""; + } + if (newValue.equals(currentValue)) { + return; + } + + if (editingName) { + localServerData.customName = newValue; + } else { + localServerData.personalNote = newValue; + } + VPNServersPersistentData.getInstance().updateServer(localServerData); + + HelperFunctions.showToast(getContext().getString(R.string.tmp_edit_value_changes_made_confirmation), true); + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/ManualServerModalWindow.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/ManualServerModalWindow.java new file mode 100644 index 000000000..cab1ee98c --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/ManualServerModalWindow.java @@ -0,0 +1,154 @@ +package com.skywire.skycoin.vpn.controls; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.KeyEvent; +import android.view.View; +import android.view.Window; +import android.view.inputmethod.EditorInfo; +import android.widget.EditText; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.extensible.ClickEvent; +import com.skywire.skycoin.vpn.helpers.HelperFunctions; +import com.skywire.skycoin.vpn.objects.LocalServerData; +import com.skywire.skycoin.vpn.objects.ManualVpnServerData; +import com.skywire.skycoin.vpn.vpn.VPNServersPersistentData; + +import skywiremob.Skywiremob; + +public class ManualServerModalWindow extends Dialog implements ClickEvent, TextWatcher { + public interface Confirmed { + void confirmed(LocalServerData server); + } + + private EditText editPk; + private EditText editPassword; + private EditText editName; + private EditText editNote; + private ModalWindowButton buttonCancel; + private ModalWindowButton buttonConfirm; + + private Confirmed event; + private boolean hasError; + + public ManualServerModalWindow(Context ctx, Confirmed event) { + super(ctx); + + this.event = event; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_NO_TITLE); + setContentView(R.layout.view_manual_server_modal); + + editPk = findViewById(R.id.editPk); + editPassword = findViewById(R.id.editPassword); + editName = findViewById(R.id.editName); + editNote = findViewById(R.id.editNote); + buttonCancel = findViewById(R.id.buttonCancel); + buttonConfirm = findViewById(R.id.buttonConfirm); + + editPk.addTextChangedListener(this); + + editPk.setImeOptions(EditorInfo.IME_ACTION_NEXT); + editName.setImeOptions(EditorInfo.IME_ACTION_NEXT); + editNote.setImeOptions(EditorInfo.IME_ACTION_DONE); + + editPk.setSelection(editName.getText().length()); + + editNote.setOnEditorActionListener((v, actionId, event) -> { + if ( + actionId == EditorInfo.IME_ACTION_DONE || + (event != null && event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_ENTER) + ) { + if (!hasError) { + process(); + dismiss(); + } + + return true; + } + + return false; + }); + + buttonCancel.setClickEventListener(this); + buttonConfirm.setClickEventListener(this); + + buttonConfirm.setEnabled(false); + hasError = true; + + HelperFunctions.configureModalWindow(this); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { } + @Override + public void afterTextChanged(Editable s) { } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + hasError = false; + if (editPk.getText().length() < 66) { + editPk.setError(getContext().getText(R.string.add_server_pk_length_error)); + hasError = true; + } else if (Skywiremob.isPKValid(editPk.getText().toString()).getCode() != Skywiremob.ErrCodeNoError) { + editPk.setError(getContext().getText(R.string.add_server_pk_invalid_error)); + hasError = true; + } + + if (hasError) { + buttonConfirm.setEnabled(false); + } else { + buttonConfirm.setEnabled(true); + } + } + + @Override + public void onClick(View view) { + if (view.getId() == R.id.buttonConfirm) { + process(); + } + + dismiss(); + } + + private void process() { + if (hasError) { + return; + } + + LocalServerData savedVersion = VPNServersPersistentData.getInstance().getSavedVersion(editPk.getText().toString().trim()); + + ManualVpnServerData serverData = new ManualVpnServerData(); + serverData.pk = editPk.getText().toString().trim(); + + String password = editPassword.getText().toString(); + if (password != null && !password.equals("")) { + serverData.password = password; + } + + if (editName.getText() != null && !editName.getText().toString().trim().equals("")) { + serverData.name = editName.getText().toString().trim(); + } else if (savedVersion != null && savedVersion.customName != null && !savedVersion.customName.equals("")) { + serverData.name = savedVersion.customName; + } + + if (editNote.getText() != null && !editNote.getText().toString().trim().equals("")) { + serverData.note = editNote.getText().toString().trim(); + } else if (savedVersion != null && savedVersion.personalNote != null && !savedVersion.personalNote.equals("")) { + serverData.note = savedVersion.personalNote; + } + + LocalServerData localServerData = VPNServersPersistentData.getInstance().processFromManual(serverData); + if (event != null) { + event.confirmed(localServerData); + } + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/ModalBase.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/ModalBase.java new file mode 100644 index 000000000..cfb280343 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/ModalBase.java @@ -0,0 +1,115 @@ +package com.skywire.skycoin.vpn.controls; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.TextView; + +import com.skywire.skycoin.vpn.R; + +public class ModalBase extends FrameLayout { + public ModalBase(Context context) { + super(context); + Initialize(context, null); + } + public ModalBase(Context context, AttributeSet attrs) { + super(context, attrs); + Initialize(context, attrs); + } + public ModalBase(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + Initialize(context, attrs); + } + + private FrameLayout mainContainer; + private TextView textTitle; + private FrameLayout contentArea; + + private void Initialize (Context context, AttributeSet attrs) { + LayoutInflater inflater = (LayoutInflater)context.getSystemService (Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_modal_base, this, true); + + mainContainer = findViewById(R.id.mainContainer); + textTitle = findViewById(R.id.textTitle); + contentArea = findViewById(R.id.contentArea); + + mainContainer.setClipToOutline(true); + + if (attrs != null) { + TypedArray attributes = context.getTheme().obtainStyledAttributes( + attrs, + R.styleable.ModalBase, + 0, 0 + ); + + String title = attributes.getString(R.styleable.ModalBase_title); + if (title != null) { + textTitle.setText(title); + } + + boolean removeInternalPadding = attributes.getBoolean(R.styleable.ModalBase_remove_internal_padding, false); + if (removeInternalPadding) { + contentArea.setPadding(0, 0, 0, 0); + } + + attributes.recycle(); + } + } + + public void setTitle(int resourceId) { + textTitle.setText(resourceId); + } + + public void setTitleString(String title) { + textTitle.setText(title); + } + + @Override + public void addView(View child) { + if (contentArea != null) { + contentArea.addView(child); + } else { + super.addView(child); + } + } + + @Override + public void addView(View child, int index) { + if (contentArea != null) { + contentArea.addView(child, index); + } else { + super.addView(child, index); + } + } + + @Override + public void addView(View child, ViewGroup.LayoutParams params) { + if (contentArea != null) { + contentArea.addView(child, params); + } else { + super.addView(child, params); + } + } + + @Override + public void addView(View child, int width, int height) { + if (contentArea != null) { + contentArea.addView(child, width, height); + } else { + super.addView(child, width, height); + } + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + if (contentArea != null) { + contentArea.addView(child, index, params); + } else { + super.addView(child, index, params); + } + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/ModalWindowButton.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/ModalWindowButton.java new file mode 100644 index 000000000..762fe9550 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/ModalWindowButton.java @@ -0,0 +1,93 @@ +package com.skywire.skycoin.vpn.controls; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.RippleDrawable; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.TextView; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.extensible.ButtonBase; + +public class ModalWindowButton extends ButtonBase implements View.OnTouchListener { + private FrameLayout mainContainer; + private FrameLayout effectContainer; + private TextView text; + + private RippleDrawable rippleDrawable; + + public ModalWindowButton(Context context) { + super(context); + } + public ModalWindowButton(Context context, AttributeSet attrs) { + super(context, attrs); + } + public ModalWindowButton(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void Initialize (Context context, AttributeSet attrs) { + LayoutInflater inflater = (LayoutInflater)context.getSystemService (Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_modal_window_button, this, true); + + mainContainer = this.findViewById (R.id.mainContainer); + effectContainer = this.findViewById (R.id.effectContainer); + text = this.findViewById (R.id.text); + + mainContainer.setClipToOutline(true); + + if (attrs != null) { + TypedArray attributes = context.getTheme().obtainStyledAttributes( + attrs, + R.styleable.ModalWindowButton, + 0, 0 + ); + + String textForButton = attributes.getString(R.styleable.ModalWindowButton_text); + if (textForButton != null) { + text.setText(textForButton); + } + + if (attributes.getBoolean(R.styleable.ModalWindowButton_use_secondary_color, false)) { + mainContainer.setBackgroundResource(R.drawable.modal_button_secondary_background); + effectContainer.setBackgroundResource(R.drawable.modal_button_secondary_ripple); + } + + attributes.recycle(); + } + + rippleDrawable = (RippleDrawable) effectContainer.getBackground(); + + setOnTouchListener(this); + setViewForCheckingClicks(this); + } + + public void setText(int resourceId) { + text.setText(resourceId); + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (rippleDrawable != null) { + rippleDrawable.setHotspot(event.getX(), event.getY()); + } + + return false; + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + + if (enabled) { + this.setAlpha(1); + } else { + this.setAlpha(0.35f); + } + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/Select.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/Select.java new file mode 100644 index 000000000..9f3f8b949 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/Select.java @@ -0,0 +1,143 @@ +package com.skywire.skycoin.vpn.controls; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.widget.EditText; +import android.widget.FrameLayout; + +import androidx.core.content.ContextCompat; + +import com.google.android.material.textfield.TextInputLayout; +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.controls.options.OptionsItem; +import com.skywire.skycoin.vpn.controls.options.OptionsModalWindow; +import com.skywire.skycoin.vpn.helpers.ClickTimeManagement; + +import java.util.ArrayList; + +public class Select extends FrameLayout implements View.OnTouchListener, View.OnClickListener { + public static class SelectOption { + public String text; + public String value; + public Integer iconId; + } + + private TextInputLayout container; + private EditText edit; + private FrameLayout clickArea; + + private ArrayList options; + private int selectedIndex = 0; + private ClickTimeManagement buttonTimeManager = new ClickTimeManagement(); + + public Select(Context context) { + super(context); + Initialize(context, null); + } + public Select(Context context, AttributeSet attrs) { + super(context, attrs); + Initialize(context, attrs); + } + public Select(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + Initialize(context, attrs); + } + + protected void Initialize (Context context, AttributeSet attrs) { + LayoutInflater inflater = (LayoutInflater)context.getSystemService (Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_select, this, true); + + container = this.findViewById (R.id.container); + edit = this.findViewById (R.id.edit); + clickArea = this.findViewById (R.id.clickArea); + + if (attrs != null) { + TypedArray attributes = context.getTheme().obtainStyledAttributes( + attrs, + R.styleable.Select, + 0, 0 + ); + + String hint = attributes.getString(R.styleable.Select_hint); + if (hint != null) { + this.container.setHint(hint); + } + + attributes.recycle(); + } + + clickArea.setOnTouchListener(this); + clickArea.setOnClickListener(this); + } + + public void setValues(ArrayList options, int selectedIndex) { + this.options = options; + this.selectedIndex = selectedIndex; + + updateContent(); + } + + private void updateContent() { + SelectOption currentOption = options.get(selectedIndex); + + Drawable leftDrawable = null; + if (currentOption.iconId != null) { + leftDrawable = ContextCompat.getDrawable(getContext(), currentOption.iconId); + leftDrawable.setBounds(0, 0, leftDrawable.getIntrinsicWidth(), leftDrawable.getIntrinsicHeight()); + } + Drawable[] drawables = edit.getCompoundDrawables(); + edit.setCompoundDrawables(leftDrawable, drawables[1], drawables[2], drawables[3]); + + if (currentOption.iconId != null) { + edit.setText(" " + currentOption.text); + } else { + edit.setText(currentOption.text); + } + } + + public String getSelectedValue() { + return options.get(selectedIndex).value; + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + setAlpha(0.5f); + } else if (event.getAction() == MotionEvent.ACTION_CANCEL || event.getAction() == MotionEvent.ACTION_POINTER_UP || event.getAction() == MotionEvent.ACTION_UP) { + setAlpha(1f); + } + + return false; + } + + @Override + public void onClick(View view) { + if (!buttonTimeManager.canClick()) { + return; + } + + buttonTimeManager.informClickMade(); + + ArrayList optionsToShow = new ArrayList(); + + for (SelectOption option : options) { + OptionsItem.SelectableOption optionToShow = new OptionsItem.SelectableOption(); + optionToShow.drawableId = option.iconId; + optionToShow.label = option.text; + + optionsToShow.add(optionToShow); + } + + OptionsModalWindow modal = new OptionsModalWindow(getContext(), null, optionsToShow, (int selectedOption) -> { + selectedIndex = selectedOption; + updateContent(); + }); + + modal.show(); + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/ServerInfoModalWindow.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/ServerInfoModalWindow.java new file mode 100644 index 000000000..83c81e6fe --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/ServerInfoModalWindow.java @@ -0,0 +1,276 @@ +package com.skywire.skycoin.vpn.controls; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.ForegroundColorSpan; +import android.text.style.RelativeSizeSpan; +import android.view.View; +import android.view.Window; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.core.content.res.ResourcesCompat; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.activities.servers.ServerLists; +import com.skywire.skycoin.vpn.activities.servers.VpnServerForList; +import com.skywire.skycoin.vpn.extensible.ClickEvent; +import com.skywire.skycoin.vpn.helpers.CountriesList; +import com.skywire.skycoin.vpn.helpers.HelperFunctions; +import com.skywire.skycoin.vpn.helpers.MaterialFontSpan; +import com.skywire.skycoin.vpn.objects.ServerFlags; +import com.skywire.skycoin.vpn.vpn.VPNServersPersistentData; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; + +public class ServerInfoModalWindow extends Dialog implements ClickEvent { + private ForegroundColorSpan lightColorSpan = + new ForegroundColorSpan(ResourcesCompat.getColor(getContext().getResources(), R.color.modal_window_light_text, null)); + private ForegroundColorSpan superLightColorSpan = + new ForegroundColorSpan(ResourcesCompat.getColor(getContext().getResources(), R.color.modal_window_super_light_text, null)); + private DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd hh:mm a"); + + private TextView textName; + private TextView textCustomName; + private TextView textPk; + private TextView textNote; + private TextView textPersonalNote; + private TextView textLastTimeUsed; + + private TextView textCountry; + private TextView textCountryCode; + private TextView textLocation; + + // TODO: for currently commented fields, must be deleted or reactivated depending on what happens to the fields. + /* + private LinearLayout connectivityContainer; + private TextView textCongestion; + private TextView textCongestionRating; + private TextView textLatency; + private TextView textLatencyRating; + private TextView textHops; + */ + + private LinearLayout specialContainer; + private TextView textIsCurrent; + private TextView textIsFavorite; + private TextView textBlocked; + private TextView textInHistory; + private TextView textEnteredManually; + private TextView textHasPassword; + + private ModalWindowButton buttonClose; + + private VpnServerForList server; + private ServerLists listType; + + public ServerInfoModalWindow(Context ctx, VpnServerForList server, ServerLists listType) { + super(ctx); + + this.server = server; + this.listType = listType; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_NO_TITLE); + setContentView(R.layout.view_server_info_modal); + + textName = findViewById(R.id.textName); + textCustomName = findViewById(R.id.textCustomName); + textPk = findViewById(R.id.textPk); + textNote = findViewById(R.id.textNote); + textPersonalNote = findViewById(R.id.textPersonalNote); + textLastTimeUsed = findViewById(R.id.textLastTimeUsed); + + textCountry = findViewById(R.id.textCountry); + textCountryCode = findViewById(R.id.textCountryCode); + textLocation = findViewById(R.id.textLocation); + + // TODO: for currently commented fields, must be deleted or reactivated depending on what happens to the fields. + /* + connectivityContainer = findViewById(R.id.connectivityContainer); + textCongestion = findViewById(R.id.textCongestion); + textCongestionRating = findViewById(R.id.textCongestionRating); + textLatency = findViewById(R.id.textLatency); + textLatencyRating = findViewById(R.id.textLatencyRating); + textHops = findViewById(R.id.textHops); + */ + + specialContainer = findViewById(R.id.specialContainer); + textIsCurrent = findViewById(R.id.textIsCurrent); + textIsFavorite = findViewById(R.id.textIsFavorite); + textBlocked = findViewById(R.id.textBlocked); + textInHistory = findViewById(R.id.textInHistory); + textEnteredManually = findViewById(R.id.textEnteredManually); + textHasPassword = findViewById(R.id.textHasPassword); + + buttonClose = findViewById(R.id.buttonClose); + + putValue(textName, R.string.server_info_name, server.name, null, null); + putValue(textCustomName, R.string.server_info_custom_name, server.customName, null, null); + putValue(textPk, R.string.server_info_pk, server.pk, null, null); + if ((server.note != null && !server.note.trim().equals("")) && (server.personalNote != null && !server.personalNote.trim().equals(""))) { + putValue(textNote, R.string.server_info_original_note, server.note, null, null); + putValue(textPersonalNote, R.string.server_info_personal_note, server.personalNote, null, null); + } else if (server.note != null && !server.note.trim().equals("")) { + putValue(textNote, R.string.server_info_note, server.note, null, null); + textPersonalNote.setVisibility(View.GONE); + } else if (server.personalNote != null && !server.personalNote.trim().equals("")) { + putValue(textPersonalNote, R.string.server_info_note, server.personalNote, null, null); + textNote.setVisibility(View.GONE); + } else { + putValue(textNote, R.string.server_info_note, null, null, null); + textPersonalNote.setVisibility(View.GONE); + } + if (server.inHistory) { + putValue(textLastTimeUsed, R.string.server_info_last_time_used, dateFormat.format(server.lastUsed), null, null); + } else { + textLastTimeUsed.setVisibility(View.GONE); + } + + putValue(textCountry, R.string.server_info_country, CountriesList.getCountryName(server.countryCode), null, null); + if (!server.countryCode.toUpperCase().equals("ZZ")) { + putValue(textCountryCode, R.string.server_info_country_code, server.countryCode.toUpperCase(), null, null); + } else { + textCountryCode.setVisibility(View.GONE); + } + putValue(textLocation, R.string.server_info_location, server.location, null, null); + + // TODO: for currently commented fields, must be deleted or reactivated depending on what happens to the fields. + /* + if (listType == ServerLists.Public) { + putValue(textCongestion, R.string.server_info_congestion, + HelperFunctions.zeroDecimalsFormatter.format(server.congestion) + "%", null, null + ); + putValue(textCongestionRating, R.string.server_info_congestion_rating, + getContext().getText(ServerRatings.getTextForRating(server.congestionRating)).toString(), getRatingColor(server.congestionRating), null + ); + putValue(textLatency, R.string.server_info_latency, + HelperFunctions.getLatencyValue(server.latency), null, null + ); + putValue(textLatencyRating, R.string.server_info_latency_rating, + getContext().getText(ServerRatings.getTextForRating(server.latencyRating)).toString(), getRatingColor(server.latencyRating), null + ); + putValue(textHops, R.string.server_info_hops, + server.hops + "", null, null + ); + } else { + connectivityContainer.setVisibility(View.GONE); + } + */ + + boolean hasSpecialCondition = false; + boolean isTheCurrentServer = VPNServersPersistentData.getInstance().getCurrentServer() != null && + VPNServersPersistentData.getInstance().getCurrentServer().pk.toLowerCase().equals(server.pk.toLowerCase()); + + if (isTheCurrentServer) { + putValue(textIsCurrent, R.string.server_info_is_current, getBooleanString(true), null, "\ue876"); + hasSpecialCondition = true; + } else { + textIsCurrent.setVisibility(View.GONE); + } + if (server.flag == ServerFlags.Favorite) { + ForegroundColorSpan iconColor = new ForegroundColorSpan(ResourcesCompat.getColor(getContext().getResources(),R.color.yellow, null)); + putValue(textIsFavorite, R.string.server_info_is_favorite, getBooleanString(true), iconColor, "\ue838"); + hasSpecialCondition = true; + } else { + textIsFavorite.setVisibility(View.GONE); + } + if (server.flag == ServerFlags.Blocked) { + ForegroundColorSpan iconColor = new ForegroundColorSpan(ResourcesCompat.getColor(getContext().getResources(),R.color.red, null)); + putValue(textBlocked, R.string.server_info_is_blocked, getBooleanString(true), iconColor, "\ue14c"); + hasSpecialCondition = true; + } else { + textBlocked.setVisibility(View.GONE); + } + if (server.inHistory && !isTheCurrentServer) { + putValue(textInHistory, R.string.server_info_is_in_history, getBooleanString(true), null, "\ue889"); + hasSpecialCondition = true; + } else { + textInHistory.setVisibility(View.GONE); + } + if (server.enteredManually) { + putValue(textEnteredManually, R.string.server_info_entered_manually, getBooleanString(true), null, null); + hasSpecialCondition = true; + } else { + textEnteredManually.setVisibility(View.GONE); + } + if (server.enteredManually && server.hasPassword) { + putValue(textHasPassword, R.string.server_info_has_password, getBooleanString(true), null, "\ue899"); + hasSpecialCondition = true; + } else { + textHasPassword.setVisibility(View.GONE); + } + if (!hasSpecialCondition) { + specialContainer.setVisibility(View.GONE); + } + + buttonClose.setClickEventListener(this); + + HelperFunctions.configureModalWindow(this); + } + + @Override + public void onClick(View view) { + dismiss(); + } + + private void putValue(TextView textView, int titleResurce, String value, ForegroundColorSpan valueColor, String icon) { + SpannableStringBuilder finalText = new SpannableStringBuilder(getContext().getString(titleResurce)); + finalText.setSpan(lightColorSpan, 0, finalText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + finalText.append("\n"); + int initialValuePos = finalText.length(); + + if (value != null && !value.trim().equals("")) { + if (icon == null) { + finalText.append(value); + + if (valueColor != null) { + finalText.setSpan(valueColor, initialValuePos, finalText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } else { + finalText.append(icon + " "); + finalText.setSpan(new MaterialFontSpan(getContext()), initialValuePos, finalText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + finalText.setSpan(new RelativeSizeSpan(0.75f), initialValuePos, finalText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + if (valueColor != null) { + finalText.setSpan(valueColor, initialValuePos, finalText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + finalText.append(value); + } + } else { + finalText.append(getContext().getString(R.string.server_info_without_value)); + finalText.setSpan(superLightColorSpan, initialValuePos, finalText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + textView.setText(finalText); + } + + private String getBooleanString(boolean value) { + if (value) { + return getContext().getText(R.string.general_yes).toString(); + } + + return getContext().getText(R.string.general_no).toString(); + } + + // TODO: for currently commented fields, must be deleted or reactivated depending on what happens to the fields. + /* + private ForegroundColorSpan getRatingColor(ServerRatings rating) { + if (rating == ServerRatings.Gold) { + return new ForegroundColorSpan(ResourcesCompat.getColor(getContext().getResources(), R.color.gold, null)); + } else if (rating == ServerRatings.Silver) { + return new ForegroundColorSpan(ResourcesCompat.getColor(getContext().getResources(), R.color.silver, null)); + } + + return new ForegroundColorSpan(ResourcesCompat.getColor(getContext().getResources(), R.color.bronze, null)); + } + */ +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/ServerName.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/ServerName.java new file mode 100644 index 000000000..eabbcf599 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/ServerName.java @@ -0,0 +1,151 @@ +package com.skywire.skycoin.vpn.controls; + +import android.content.Context; +import android.content.res.TypedArray; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.ForegroundColorSpan; +import android.text.style.RelativeSizeSpan; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.widget.FrameLayout; +import android.widget.TextView; + +import androidx.core.content.res.ResourcesCompat; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.activities.servers.ServerLists; +import com.skywire.skycoin.vpn.activities.servers.VpnServerForList; +import com.skywire.skycoin.vpn.helpers.AlphaSpan; +import com.skywire.skycoin.vpn.helpers.HelperFunctions; +import com.skywire.skycoin.vpn.helpers.MaterialFontSpan; +import com.skywire.skycoin.vpn.objects.ServerFlags; +import com.skywire.skycoin.vpn.vpn.VPNServersPersistentData; + +public class ServerName extends FrameLayout { + private TextView text; + + private String defaultName = ""; + private boolean showConfigIcon = false; + + public ServerName(Context context) { + super(context); + Initialize(context, null); + } + public ServerName(Context context, AttributeSet attrs) { + super(context, attrs); + Initialize(context, attrs); + } + public ServerName(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + Initialize(context, attrs); + } + + private void Initialize(Context context, AttributeSet attrs) { + LayoutInflater inflater = (LayoutInflater)context.getSystemService (Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_server_name, this, true); + + text = this.findViewById (R.id.text); + + if (attrs != null) { + TypedArray attributes = context.getTheme().obtainStyledAttributes( + attrs, + R.styleable.ServerName, + 0, 0 + ); + + boolean centerText = attributes.getBoolean(R.styleable.ServerName_center_text, false); + if (centerText) { + text.setGravity(Gravity.CENTER_HORIZONTAL); + } + + String defaultName = attributes.getString(R.styleable.ServerName_default_name); + if (defaultName != null) { + this.defaultName = defaultName; + text.setText(defaultName); + } + + float textSize = attributes.getDimensionPixelSize(R.styleable.ServerName_text_size, -1); + if (textSize != -1) { + text.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); + } + + showConfigIcon = attributes.getBoolean(R.styleable.ServerName_show_config_icon, false); + + attributes.recycle(); + } + } + + public void setServer(VpnServerForList server, ServerLists listType, boolean doNotMarkCurrent) { + if (server == null) { + text.setText(defaultName); + + return; + } + + MaterialFontSpan materialFontSpan = new MaterialFontSpan(getContext()); + RelativeSizeSpan relativeSizeSpan = new RelativeSizeSpan(0.75f); + + int initialicons = 0; + boolean isCurrentServer = VPNServersPersistentData.getInstance().getCurrentServer() != null && + server.pk.toLowerCase().equals(VPNServersPersistentData.getInstance().getCurrentServer().pk.toLowerCase()); + + SpannableStringBuilder finalText = new SpannableStringBuilder(""); + + if (isCurrentServer && !doNotMarkCurrent) { + finalText.append("\ue876 "); + initialicons += 1; + } + if (server.flag == ServerFlags.Blocked && listType != ServerLists.Blocked) { + finalText.append("\ue14c "); + finalText.setSpan(new ForegroundColorSpan( + ResourcesCompat.getColor(getResources(),R.color.red, null)), + initialicons * 2, + (initialicons * 2) + 2, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ); + initialicons += 1; + } + if (server.flag == ServerFlags.Favorite && listType != ServerLists.Favorites) { + finalText.append("\ue838 "); + finalText.setSpan(new ForegroundColorSpan( + ResourcesCompat.getColor(getResources(),R.color.yellow, null)), + initialicons * 2, + (initialicons * 2) + 2, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ); + initialicons += 1; + } + if (server.inHistory && listType != ServerLists.History && !isCurrentServer) { + finalText.append("\ue889 "); + initialicons += 1; + } + if (server.hasPassword) { + finalText.append("\ue899 "); + initialicons += 1; + } + + if (initialicons != 0) { + finalText.setSpan(materialFontSpan, 0, initialicons * 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + finalText.setSpan(relativeSizeSpan, 0, initialicons * 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + finalText.append(HelperFunctions.getServerName(server, defaultName)); + + if (showConfigIcon) { + finalText.append(" \ue8b8"); + + materialFontSpan = new MaterialFontSpan(getContext()); + relativeSizeSpan = new RelativeSizeSpan(0.75f); + AlphaSpan alphaSpan = new AlphaSpan(128); + + finalText.setSpan(materialFontSpan, finalText.length() - 2, finalText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + finalText.setSpan(relativeSizeSpan, finalText.length() - 2, finalText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + finalText.setSpan(alphaSpan, finalText.length() - 2, finalText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + text.setText(finalText); + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/ServerNotesModalWindow.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/ServerNotesModalWindow.java new file mode 100644 index 000000000..92007f909 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/ServerNotesModalWindow.java @@ -0,0 +1,69 @@ +package com.skywire.skycoin.vpn.controls; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.view.View; +import android.view.Window; +import android.widget.TextView; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.activities.servers.VpnServerForList; +import com.skywire.skycoin.vpn.extensible.ClickEvent; +import com.skywire.skycoin.vpn.helpers.HelperFunctions; + +public class ServerNotesModalWindow extends Dialog implements ClickEvent { + private TextView textNoteTitle; + private TextView textNote; + private TextView textPersonalNoteTitle; + private TextView textPersonalNote; + + private ModalWindowButton buttonClose; + + private VpnServerForList server; + + public ServerNotesModalWindow(Context ctx, VpnServerForList server) { + super(ctx); + + this.server = server; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_NO_TITLE); + setContentView(R.layout.view_server_notes_modal); + + textNoteTitle = findViewById(R.id.textNoteTitle); + textNote = findViewById(R.id.textNote); + textPersonalNoteTitle = findViewById(R.id.textPersonalNoteTitle); + textPersonalNote = findViewById(R.id.textPersonalNote); + buttonClose = findViewById(R.id.buttonClose); + + if ((server.note != null && !server.note.trim().equals("")) && (server.personalNote != null && !server.personalNote.trim().equals(""))) { + textNote.setText(server.note); + textPersonalNote.setText(server.personalNote); + } else { + textNoteTitle.setVisibility(View.GONE); + textPersonalNoteTitle.setVisibility(View.GONE); + textPersonalNote.setVisibility(View.GONE); + + if (server.note != null && !server.note.trim().equals("")) { + textNote.setText(server.note); + } else if (server.personalNote != null && !server.personalNote.trim().equals("")) { + textNote.setText(server.personalNote); + } else { + textNote.setVisibility(View.GONE); + } + } + + buttonClose.setClickEventListener(this); + + HelperFunctions.configureModalWindow(this); + } + + @Override + public void onClick(View view) { + dismiss(); + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/ServerPasswordModalWindow.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/ServerPasswordModalWindow.java new file mode 100644 index 000000000..b2407652b --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/ServerPasswordModalWindow.java @@ -0,0 +1,101 @@ +package com.skywire.skycoin.vpn.controls; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.KeyEvent; +import android.view.View; +import android.view.Window; +import android.view.inputmethod.EditorInfo; +import android.widget.EditText; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.activities.servers.VpnServerForList; +import com.skywire.skycoin.vpn.extensible.ClickEvent; +import com.skywire.skycoin.vpn.helpers.HelperFunctions; +import com.skywire.skycoin.vpn.objects.LocalServerData; +import com.skywire.skycoin.vpn.vpn.VPNServersPersistentData; + +public class ServerPasswordModalWindow extends Dialog implements ClickEvent, TextWatcher { + private EditText editPassword; + private ModalWindowButton buttonCancel; + private ModalWindowButton buttonConfirm; + + private VpnServerForList server; + + public ServerPasswordModalWindow(Context ctx, VpnServerForList server) { + super(ctx); + + this.server = server; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_NO_TITLE); + setContentView(R.layout.view_server_password_modal); + + editPassword = findViewById(R.id.editPassword); + buttonCancel = findViewById(R.id.buttonCancel); + buttonConfirm = findViewById(R.id.buttonConfirm); + + editPassword.setOnEditorActionListener((v, actionId, event) -> { + if ( + actionId == EditorInfo.IME_ACTION_DONE || + (event != null && event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_ENTER) + ) { + if (buttonConfirm.isEnabled()) { + makeChange(); + dismiss(); + } + + return true; + } + + return false; + }); + + editPassword.addTextChangedListener(this); + + buttonCancel.setClickEventListener(this); + buttonConfirm.setClickEventListener(this); + + buttonConfirm.setEnabled(false); + + HelperFunctions.configureModalWindow(this); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { } + @Override + public void afterTextChanged(Editable s) { } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (editPassword.getText() == null || editPassword.getText().toString().equals("")) { + buttonConfirm.setEnabled(false); + } else { + buttonConfirm.setEnabled(true); + } + } + + @Override + public void onClick(View view) { + if (view.getId() == R.id.buttonConfirm) { + makeChange(); + } + + dismiss(); + } + + private void makeChange() { + LocalServerData localServerData = VPNServersPersistentData.getInstance().processFromList(server); + + localServerData.password = editPassword.getText().toString(); + VPNServersPersistentData.getInstance().updateServer(localServerData); + + HelperFunctions.showToast(getContext().getString(R.string.server_password_changes_made_confirmation), true); + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/SettingsButton.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/SettingsButton.java new file mode 100644 index 000000000..b3a873540 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/SettingsButton.java @@ -0,0 +1,63 @@ +package com.skywire.skycoin.vpn.controls; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.widget.TextView; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.extensible.ButtonBase; + +public class SettingsButton extends ButtonBase implements View.OnTouchListener { + private TextView textIcon; + + public SettingsButton(Context context) { + super(context); + } + public SettingsButton(Context context, AttributeSet attrs) { + super(context, attrs); + } + public SettingsButton(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void Initialize (Context context, AttributeSet attrs) { + LayoutInflater inflater = (LayoutInflater)context.getSystemService (Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_settings_button, this, true); + + textIcon = this.findViewById (R.id.textIcon); + + if (attrs != null) { + TypedArray attributes = context.getTheme().obtainStyledAttributes( + attrs, + R.styleable.SettingsButton, + 0, 0 + ); + + boolean useNoteIcon = attributes.getBoolean(R.styleable.SettingsButton_use_note_icon, false); + if (useNoteIcon) { + textIcon.setText("\ue88f"); + } + + attributes.recycle(); + } + + setOnTouchListener(this); + setViewForCheckingClicks(this); + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + textIcon.setAlpha(0.5f); + } else if (event.getAction() == MotionEvent.ACTION_CANCEL || event.getAction() == MotionEvent.ACTION_POINTER_UP || event.getAction() == MotionEvent.ACTION_UP) { + textIcon.setAlpha(1.0f); + } + + return false; + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/Tab.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/Tab.java new file mode 100644 index 000000000..28dba6efa --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/Tab.java @@ -0,0 +1,96 @@ +package com.skywire.skycoin.vpn.controls; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.RippleDrawable; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.extensible.ButtonBase; + +public class Tab extends ButtonBase implements View.OnTouchListener { + private LinearLayout mainContainer; + private LinearLayout internalContainer; + private FrameLayout rightBorder; + private TextView textIcon; + private TextView textName; + + private RippleDrawable rippleDrawable; + + public Tab(Context context) { + super(context); + } + public Tab(Context context, AttributeSet attrs) { + super(context, attrs); + } + public Tab(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void Initialize (Context context, AttributeSet attrs) { + LayoutInflater inflater = (LayoutInflater)context.getSystemService (Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_tab, this, true); + + mainContainer = this.findViewById (R.id.mainContainer); + internalContainer = this.findViewById (R.id.internalContainer); + rightBorder = this.findViewById (R.id.rightBorder); + textIcon = this.findViewById (R.id.textIcon); + textName = this.findViewById (R.id.textName); + + rippleDrawable = (RippleDrawable) internalContainer.getBackground(); + + if (attrs != null) { + TypedArray attributes = context.getTheme().obtainStyledAttributes( + attrs, + R.styleable.Tab, + 0, 0 + ); + + String iconText = attributes.getString(R.styleable.Tab_icon_text); + if (iconText != null) { + textIcon.setText(iconText); + } + + textName.setText(attributes.getString(R.styleable.Tab_lower_text)); + + if (!attributes.getBoolean(R.styleable.Tab_show_right_border, true)) { + rightBorder.setVisibility(GONE); + } + + attributes.recycle(); + } + + setOnTouchListener(this); + setViewForCheckingClicks(this); + } + + public void changeState(boolean selected) { + if (selected) { + mainContainer.setBackgroundResource(R.color.bar_selected); + internalContainer.setBackground(null); + rippleDrawable = null; + this.setClickable(false); + } else { + mainContainer.setBackgroundResource(R.color.bar_background); + internalContainer.setBackgroundResource(R.drawable.box_ripple); + rippleDrawable = (RippleDrawable) internalContainer.getBackground(); + this.setClickable(true); + } + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (rippleDrawable != null) { + rippleDrawable.setHotspot(event.getX(), event.getY()); + } + + return false; + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/TabletTopBar.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/TabletTopBar.java new file mode 100644 index 000000000..cc8d16bef --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/TabletTopBar.java @@ -0,0 +1,119 @@ +package com.skywire.skycoin.vpn.controls; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.extensible.ClickEvent; +import com.skywire.skycoin.vpn.extensible.ClickWithIndexEvent; + +import java.io.Closeable; + +public class TabletTopBar extends FrameLayout implements ClickEvent, Closeable { + public TabletTopBar(Context context) { + super(context); + Initialize(context, null); + } + public TabletTopBar(Context context, AttributeSet attrs) { + super(context, attrs); + Initialize(context, attrs); + } + public TabletTopBar(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + Initialize(context, attrs); + } + + public static int statusTabIndex = 0; + public static int serversTabIndex = 1; + public static int settingsTabIndex = 2; + + private TabletTopBarTab tabStatus; + private TabletTopBarTab tabServers; + private TabletTopBarTab tabSettings; + private TabletTopBarStats stats; + + private ClickWithIndexEvent clickListener; + + private void Initialize (Context context, AttributeSet attrs) { + LayoutInflater inflater = (LayoutInflater)context.getSystemService (Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_tablet_top_bar, this, true); + + tabStatus = this.findViewById (R.id.tabStatus); + tabServers = this.findViewById (R.id.tabServers); + tabSettings = this.findViewById (R.id.tabSettings); + stats = this.findViewById (R.id.stats); + + stats.setVisibility(INVISIBLE); + + tabStatus.setClickEventListener(this); + tabServers.setClickEventListener(this); + tabSettings.setClickEventListener(this); + } + + public void onResume() { + if (stats.getVisibility() == VISIBLE) { + stats.onResume(); + } + } + + public void onPause() { + if (stats.getVisibility() == VISIBLE) { + stats.onPause(); + } + } + + public void setSelectedTab(int tabIndex) { + tabStatus.setSelected(false); + tabServers.setSelected(false); + tabSettings.setSelected(false); + + if (tabIndex == statusTabIndex) { + tabStatus.setSelected(true); + + if (stats.getVisibility() == VISIBLE) { + stats.setVisibility(INVISIBLE); + stats.onPause(); + } + } else if (tabIndex == serversTabIndex) { + tabServers.setSelected(true); + + if (stats.getVisibility() != VISIBLE) { + stats.setVisibility(VISIBLE); + stats.onResume(); + } + } else if (tabIndex == settingsTabIndex) { + tabSettings.setSelected(true); + + if (stats.getVisibility() != VISIBLE) { + stats.setVisibility(VISIBLE); + stats.onResume(); + } + } + } + + public void setClickWithIndexEventListener(ClickWithIndexEvent listener) { + clickListener = listener; + } + + @Override + public void onClick(View view) { + if (clickListener != null) { + if (view.getId() == R.id.tabStatus) { + clickListener.onClickWithIndex(statusTabIndex, null); + } else if (view.getId() == R.id.tabServers) { + clickListener.onClickWithIndex(serversTabIndex, null); + } else if (view.getId() == R.id.tabSettings) { + clickListener.onClickWithIndex(settingsTabIndex, null); + } + } + } + + @Override + public void close() { + stats.close(); + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/TabletTopBarStats.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/TabletTopBarStats.java new file mode 100644 index 000000000..1b4503e73 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/TabletTopBarStats.java @@ -0,0 +1,148 @@ +package com.skywire.skycoin.vpn.controls; + +import android.animation.Animator; +import android.animation.AnimatorInflater; +import android.animation.AnimatorSet; +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.widget.FrameLayout; +import android.widget.TextView; + +import androidx.core.content.ContextCompat; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.helpers.Globals; +import com.skywire.skycoin.vpn.helpers.HelperFunctions; +import com.skywire.skycoin.vpn.vpn.VPNCoordinator; +import com.skywire.skycoin.vpn.vpn.VPNGeneralPersistentData; +import com.skywire.skycoin.vpn.vpn.VPNStates; + +import java.io.Closeable; + +import io.reactivex.rxjava3.disposables.Disposable; + +public class TabletTopBarStats extends FrameLayout implements Animator.AnimatorListener, Closeable { + private TextView textConnectionIconAnim; + private TextView textConnectionIcon; + private TextView textConnection; + private TextView textLatency; + private TextView textUploadSpeed; + private TextView textDownloadSpeed; + + private VPNStates currentState = VPNStates.OFF; + private VPNCoordinator.ConnectionStats currentStats = new VPNCoordinator.ConnectionStats(); + private Globals.DataUnits dataUnits = VPNGeneralPersistentData.getDataUnits(); + + private AnimatorSet animSet; + + private boolean animPaused = false; + private boolean closed = false; + private Disposable eventsSubscription; + private Disposable statsSubscription; + private Disposable dataUnitsSubscription; + + public TabletTopBarStats(Context context) { + super(context); + Initialize(context, null); + } + public TabletTopBarStats(Context context, AttributeSet attrs) { + super(context, attrs); + Initialize(context, attrs); + } + public TabletTopBarStats(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + Initialize(context, attrs); + } + + protected void Initialize (Context context, AttributeSet attrs) { + LayoutInflater inflater = (LayoutInflater)context.getSystemService (Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_tablet_top_bar_stats, this, true); + + textConnectionIconAnim = this.findViewById (R.id.textConnectionIconAnim); + textConnectionIcon = this.findViewById (R.id.textConnectionIcon); + textConnection = this.findViewById (R.id.textConnection); + textLatency = this.findViewById (R.id.textLatency); + textUploadSpeed = this.findViewById (R.id.textUploadSpeed); + textDownloadSpeed = this.findViewById (R.id.textDownloadSpeed); + + animSet = (AnimatorSet) AnimatorInflater.loadAnimator(getContext(), R.animator.anim_state); + animSet.setTarget(textConnectionIconAnim); + } + + public void onResume() { + if (!closed) { + animPaused = false; + animSet.addListener(this); + animSet.start(); + + updateData(); + + eventsSubscription = VPNCoordinator.getInstance().getEventsObservable().subscribe(response -> { + currentState = response.state; + updateData(); + }); + + statsSubscription = VPNCoordinator.getInstance().getConnectionStats().subscribe(stats -> { + currentStats = stats; + updateData(); + }); + + dataUnitsSubscription = VPNGeneralPersistentData.getDataUnitsObservable().subscribe(response -> { + dataUnits = response; + updateData(); + }); + } + } + + public void onPause() { + animPaused = true; + animSet.removeAllListeners(); + animSet.cancel(); + + eventsSubscription.dispose(); + statsSubscription.dispose(); + dataUnitsSubscription.dispose(); + } + + @Override + public void onAnimationStart(Animator animation) { } + @Override + public void onAnimationCancel(Animator animation) { } + @Override + public void onAnimationRepeat(Animator animation) { } + @Override + public void onAnimationEnd(Animator animation) { + if (!closed && !animPaused) { + animSet.start(); + } + } + + private void updateData() { + int stateText = VPNStates.getTitleForState(currentState); + if (stateText != -1) { + textConnection.setText(stateText); + } else { + textConnection.setText("---"); + } + + int stateColor = ContextCompat.getColor(getContext(), VPNStates.getColorForStateTitle(stateText)); + textConnectionIconAnim.setTextColor(stateColor); + textConnection.setTextColor(stateColor); + textConnectionIcon.setTextColor(stateColor); + + textLatency.setText(HelperFunctions.getLatencyValue(currentStats.currentLatency)); + textDownloadSpeed.setText(HelperFunctions.computeDataAmountString(currentStats.currentDownloadSpeed, true, dataUnits != Globals.DataUnits.OnlyBytes)); + textUploadSpeed.setText(HelperFunctions.computeDataAmountString(currentStats.currentUploadSpeed, true, dataUnits != Globals.DataUnits.OnlyBytes)); + } + + @Override + public void close() { + closed = true; + + if (eventsSubscription != null) { + eventsSubscription.dispose(); + statsSubscription.dispose(); + } + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/TabletTopBarTab.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/TabletTopBarTab.java new file mode 100644 index 000000000..19ef475e1 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/TabletTopBarTab.java @@ -0,0 +1,94 @@ +package com.skywire.skycoin.vpn.controls; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.RippleDrawable; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.extensible.ButtonBase; + +public class TabletTopBarTab extends ButtonBase implements View.OnTouchListener { + private FrameLayout mainContainer; + private LinearLayout internalContainer; + private TextView textIcon; + private TextView textLabel; + + private RippleDrawable rippleDrawable; + + public TabletTopBarTab(Context context) { + super(context); + } + public TabletTopBarTab(Context context, AttributeSet attrs) { + super(context, attrs); + } + public TabletTopBarTab(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void Initialize (Context context, AttributeSet attrs) { + LayoutInflater inflater = (LayoutInflater)context.getSystemService (Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_tablet_top_bar_tab, this, true); + + mainContainer = this.findViewById (R.id.mainContainer); + internalContainer = this.findViewById (R.id.internalContainer); + textIcon = this.findViewById (R.id.textIcon); + textLabel = this.findViewById (R.id.textLabel); + + mainContainer.setClipToOutline(true); + + if (attrs != null) { + TypedArray attributes = context.getTheme().obtainStyledAttributes( + attrs, + R.styleable.TabletTopBarTab, + 0, 0 + ); + + String iconText = attributes.getString(R.styleable.TabletTopBarTab_icon_text); + if (iconText != null) { + textIcon.setText(iconText); + } + + textLabel.setText(attributes.getString(R.styleable.TabletTopBarTab_label)); + + attributes.recycle(); + } + + setOnTouchListener(this); + setViewForCheckingClicks(this); + + setSelected(false); + } + + public void setSelected(boolean selected) { + if (selected) { + textIcon.setAlpha(1f); + textLabel.setAlpha(1f); + internalContainer.setBackgroundResource(R.drawable.current_server_rounded_box); + rippleDrawable = null; + setClickable(false); + } else { + textIcon.setAlpha(0.5f); + textLabel.setAlpha(0.5f); + internalContainer.setBackgroundResource(R.drawable.current_server_ripple); + rippleDrawable = (RippleDrawable) internalContainer.getBackground(); + setClickable(true); + } + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (rippleDrawable != null) { + rippleDrawable.setHotspot(event.getX(), event.getY()); + } + + return false; + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/TopBar.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/TopBar.java new file mode 100644 index 000000000..4938b3e21 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/TopBar.java @@ -0,0 +1,94 @@ +package com.skywire.skycoin.vpn.controls; + +import android.app.Activity; +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.extensible.ClickEvent; +import com.skywire.skycoin.vpn.extensible.ClickWithIndexEvent; +import com.skywire.skycoin.vpn.helpers.UiMaterialIcons; + +public class TopBar extends LinearLayout implements ClickEvent { + public TopBar(Context context) { + super(context); + Initialize(context, null); + } + public TopBar(Context context, AttributeSet attrs) { + super(context, attrs); + Initialize(context, attrs); + } + public TopBar(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + Initialize(context, attrs); + } + + private TopBarButton buttonLeft; + private ImageView imageIcon; + private TextView textTitle; + + private ClickWithIndexEvent clickListener; + private boolean goBack = false; + + private void Initialize (Context context, AttributeSet attrs) { + LayoutInflater inflater = (LayoutInflater)context.getSystemService (Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_top_bar, this, true); + + buttonLeft = this.findViewById (R.id.buttonLeft); + imageIcon = this.findViewById (R.id.imageIcon); + textTitle = this.findViewById (R.id.textTitle); + + buttonLeft.setClickEventListener(this); + + if (attrs != null) { + TypedArray attributes = context.getTheme().obtainStyledAttributes( + attrs, + R.styleable.TopBar, + 0, 0); + + String title = attributes.getString(R.styleable.TopBar_title); + if (title == null || title.trim() == "") { + textTitle.setVisibility(GONE); + } else { + imageIcon.setVisibility(GONE); + textTitle.setText(title); + } + + int leftButtonIcon = attributes.getInteger(R.styleable.TopBar_left_button_icon, -1); + if (leftButtonIcon == 0) { + buttonLeft.setIcon(UiMaterialIcons.MENU); + } else if (leftButtonIcon == 1) { + buttonLeft.setIcon(UiMaterialIcons.BACK); + goBack = true; + } else { + buttonLeft.setVisibility(GONE); + } + + attributes.recycle(); + } else { + textTitle.setVisibility(GONE); + buttonLeft.setVisibility(GONE); + } + } + + public void setClickWithIndexEventListener(ClickWithIndexEvent listener) { + clickListener = listener; + } + + @Override + public void onClick(View view) { + if (clickListener != null) { + clickListener.onClickWithIndex(0, null); + } + + if (goBack) { + ((Activity)getContext()).finish(); + } + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/TopBarButton.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/TopBarButton.java new file mode 100644 index 000000000..2b1af828b --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/TopBarButton.java @@ -0,0 +1,60 @@ +package com.skywire.skycoin.vpn.controls; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.widget.TextView; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.extensible.ButtonBase; +import com.skywire.skycoin.vpn.helpers.UiMaterialIcons; + +public class TopBarButton extends ButtonBase { + public TopBarButton(Context context) { + super(context); + } + public TopBarButton(Context context, AttributeSet attrs) { + super(context, attrs); + } + public TopBarButton(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + private TextView textIcon; + + @Override + protected void Initialize (Context context, AttributeSet attrs) { + LayoutInflater inflater = (LayoutInflater)context.getSystemService (Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_top_bar_button, this, true); + + textIcon = this.findViewById (R.id.textIcon); + + if (attrs != null) { + TypedArray attributes = context.getTheme().obtainStyledAttributes( + attrs, + R.styleable.TopBarButton, + 0, 0); + + if (attributes.getInteger(R.styleable.TopBarButton_material_icon, 0) == 0) { + textIcon.setText("\ue5d2"); + } else { + textIcon.setText("\ue5c4"); + } + + attributes.recycle(); + } else { + textIcon.setText("\ue5d2"); + } + + setViewForCheckingClicks(this); + } + + public void setIcon(UiMaterialIcons icon) { + if (icon == UiMaterialIcons.MENU) { + textIcon.setText("\ue5d2"); + } else { + textIcon.setText("\ue5c4"); + } + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/TopTab.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/TopTab.java new file mode 100644 index 000000000..03369d5ce --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/TopTab.java @@ -0,0 +1,22 @@ +package com.skywire.skycoin.vpn.controls; + +import android.content.Context; +import android.view.LayoutInflater; +import android.widget.FrameLayout; +import android.widget.TextView; + +import com.skywire.skycoin.vpn.R; + +public class TopTab extends FrameLayout { + private TextView text; + + public TopTab(Context context, int textResource) { + super(context); + + LayoutInflater inflater = (LayoutInflater)context.getSystemService (Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_top_tab, this, true); + + text = this.findViewById (R.id.text); + text.setText(textResource); + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/options/OptionsItem.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/options/OptionsItem.java new file mode 100644 index 000000000..ff8903cec --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/options/OptionsItem.java @@ -0,0 +1,116 @@ +package com.skywire.skycoin.vpn.controls.options; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.RippleDrawable; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.extensible.ListButtonBase; + +public class OptionsItem extends ListButtonBase implements View.OnTouchListener { + public static class SelectableOption { + public String icon; + public Integer drawableId; + public String label; + public int translatableLabelId = -1; + public boolean disabled = false; + } + + private LinearLayout mainContainer; + private ImageView imageBitmap; + private TextView textIcon; + private TextView text; + + private RippleDrawable rippleDrawable; + + public OptionsItem(Context context) { + super(context); + } + public OptionsItem(Context context, AttributeSet attrs) { + super(context, attrs); + } + public OptionsItem(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void Initialize (Context context, AttributeSet attrs) { + LayoutInflater inflater = (LayoutInflater)context.getSystemService (Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_options_item, this, true); + + mainContainer = this.findViewById (R.id.mainContainer); + imageBitmap = this.findViewById (R.id.imageBitmap); + textIcon = this.findViewById (R.id.textIcon); + text = this.findViewById (R.id.text); + + rippleDrawable = (RippleDrawable) mainContainer.getBackground(); + + setOnTouchListener(this); + + if (attrs != null) { + TypedArray attributes = context.getTheme().obtainStyledAttributes( + attrs, + R.styleable.OptionsItem, + 0, 0 + ); + + String iconText = attributes.getString(R.styleable.OptionsItem_icon_text); + if (iconText != null) { + textIcon.setText(iconText); + } + + text.setText(attributes.getString(R.styleable.OptionsItem_text)); + + attributes.recycle(); + } + + setViewForCheckingClicks(this); + } + + public void setParams(SelectableOption params) { + if (params.icon != null) { + textIcon.setText(params.icon); + textIcon.setVisibility(VISIBLE); + imageBitmap.setVisibility(GONE); + } else { + textIcon.setVisibility(GONE); + + if (params.drawableId != null) { + imageBitmap.setImageResource(params.drawableId); + imageBitmap.setVisibility(VISIBLE); + } else { + imageBitmap.setVisibility(GONE); + } + } + + if (params.translatableLabelId != -1) { + text.setText(params.translatableLabelId); + } else if (params.label != null) { + text.setText(params.label); + } + + if (params.disabled) { + this.setAlpha(0.5f); + this.setClickable(false); + } else { + this.setAlpha(1f); + this.setClickable(true); + } + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (rippleDrawable != null) { + rippleDrawable.setHotspot(event.getX(), event.getY()); + } + + return false; + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/options/OptionsModalWindow.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/options/OptionsModalWindow.java new file mode 100644 index 000000000..0f7075fba --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/controls/options/OptionsModalWindow.java @@ -0,0 +1,69 @@ +package com.skywire.skycoin.vpn.controls.options; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.view.Window; +import android.widget.LinearLayout; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.controls.ModalBase; +import com.skywire.skycoin.vpn.extensible.ClickWithIndexEvent; +import com.skywire.skycoin.vpn.helpers.HelperFunctions; + +import java.util.ArrayList; + +public class OptionsModalWindow extends Dialog implements ClickWithIndexEvent { + public interface OptionSelected { + void optionSelected(int selectedIndex); + } + + private String title; + private ModalBase modalBase; + private LinearLayout container; + + private ArrayList options; + private OptionSelected event; + + public OptionsModalWindow(Context ctx, String title, ArrayList options, OptionSelected event) { + super(ctx); + + this.title = title; + this.options = options; + this.event = event; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_NO_TITLE); + setContentView(R.layout.view_options); + + modalBase = findViewById(R.id.modalBase); + container = findViewById(R.id.container); + + if (title != null) { + modalBase.setTitleString(title); + } + + int i = 0; + for (OptionsItem.SelectableOption option : options) { + OptionsItem view = new OptionsItem(getContext()); + view.setParams(option); + view.setIndex(i++); + view.setClickWithIndexEventListener(this); + container.addView(view); + } + + HelperFunctions.configureModalWindow(this); + } + + @Override + public void onClickWithIndex(int index, Void data) { + if (event != null) { + event.optionSelected(index); + } + + dismiss(); + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/extensible/ButtonBase.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/extensible/ButtonBase.java new file mode 100644 index 000000000..03c82af9d --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/extensible/ButtonBase.java @@ -0,0 +1,71 @@ +package com.skywire.skycoin.vpn.extensible; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.RelativeLayout; + +import com.skywire.skycoin.vpn.controls.BoxRowLayout; +import com.skywire.skycoin.vpn.helpers.ClickTimeManagement; +import com.skywire.skycoin.vpn.helpers.Globals; + +import java.util.concurrent.TimeUnit; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public abstract class ButtonBase extends RelativeLayout implements View.OnClickListener { + public ButtonBase(Context context) { + super(context); + Initialize(context, null); + } + public ButtonBase(Context context, AttributeSet attrs) { + super(context, attrs); + Initialize(context, attrs); + } + public ButtonBase(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + Initialize(context, attrs); + } + + private ClickEvent clickListener; + private ClickTimeManagement buttonTimeManager = new ClickTimeManagement(); + + abstract protected void Initialize (Context context, AttributeSet attrs); + + protected void setViewForCheckingClicks(View v) { + v.setOnClickListener(this); + } + + protected void setClickableBoxView(BoxRowLayout v) { + v.setClickEventListener(view -> { + if (clickListener != null) { + clickListener.onClick(this); + } + }); + } + + public void setUseBigFastClickPrevention(boolean useBigFastClickPrevention) { + if (useBigFastClickPrevention) { + buttonTimeManager.setDelay(ClickTimeManagement.normalFastClickPreventionDelay); + } else { + buttonTimeManager.setDelay(Globals.CLICK_DELAY_MS); + } + } + + public void setClickEventListener(ClickEvent listener) { + clickListener = listener; + } + + @Override + public void onClick(View view) { + if (clickListener != null && buttonTimeManager.canClick()) { + buttonTimeManager.informClickMade(); + Observable.just(1).delay(Globals.CLICK_DELAY_MS, TimeUnit.MILLISECONDS) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(v -> clickListener.onClick(this)); + } + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/extensible/ClickEvent.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/extensible/ClickEvent.java new file mode 100644 index 000000000..2de332ff9 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/extensible/ClickEvent.java @@ -0,0 +1,7 @@ +package com.skywire.skycoin.vpn.extensible; + +import android.view.View; + +public interface ClickEvent { + void onClick(View view); +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/extensible/ClickWithIndexEvent.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/extensible/ClickWithIndexEvent.java new file mode 100644 index 000000000..bbac77675 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/extensible/ClickWithIndexEvent.java @@ -0,0 +1,5 @@ +package com.skywire.skycoin.vpn.extensible; + +public interface ClickWithIndexEvent { + void onClickWithIndex(int index, T data); +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/extensible/ListButtonBase.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/extensible/ListButtonBase.java new file mode 100644 index 000000000..2bcfac359 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/extensible/ListButtonBase.java @@ -0,0 +1,81 @@ +package com.skywire.skycoin.vpn.extensible; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.RelativeLayout; + +import com.skywire.skycoin.vpn.controls.BoxRowLayout; +import com.skywire.skycoin.vpn.helpers.ClickTimeManagement; +import com.skywire.skycoin.vpn.helpers.Globals; + +import java.util.concurrent.TimeUnit; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public abstract class ListButtonBase extends RelativeLayout implements View.OnClickListener { + public ListButtonBase(Context context) { + super(context); + Initialize(context, null); + } + public ListButtonBase(Context context, AttributeSet attrs) { + super(context, attrs); + Initialize(context, attrs); + } + public ListButtonBase(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + Initialize(context, attrs); + } + + protected DataType dataForEvent; + private int index; + private ClickWithIndexEvent clickListener; + private ClickTimeManagement buttonTimeManager = new ClickTimeManagement(); + + abstract protected void Initialize (Context context, AttributeSet attrs); + + protected void setViewForCheckingClicks(View v) { + v.setOnClickListener(this); + } + + protected void setClickableBoxView(BoxRowLayout v) { + v.setClickEventListener(view -> { + if (clickListener != null) { + clickListener.onClickWithIndex(index, dataForEvent); + } + }); + } + + public void setUseBigFastClickPrevention(boolean useBigFastClickPrevention) { + if (useBigFastClickPrevention) { + buttonTimeManager.setDelay(ClickTimeManagement.normalFastClickPreventionDelay); + } else { + buttonTimeManager.setDelay(Globals.CLICK_DELAY_MS); + } + } + + public void setIndex(int index) { + this.index = index; + } + + public int getIndex() { + return index; + } + + public void setClickWithIndexEventListener(ClickWithIndexEvent listener) { + clickListener = listener; + } + + @Override + public void onClick(View view) { + if (clickListener != null && buttonTimeManager.canClick()) { + buttonTimeManager.informClickMade(); + Observable.just(1).delay(Globals.CLICK_DELAY_MS, TimeUnit.MILLISECONDS) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(v -> clickListener.onClickWithIndex(index, dataForEvent)); + } + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/extensible/ListViewHolder.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/extensible/ListViewHolder.java new file mode 100644 index 000000000..c3fe237e0 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/extensible/ListViewHolder.java @@ -0,0 +1,11 @@ +package com.skywire.skycoin.vpn.extensible; + +import android.view.View; + +import androidx.recyclerview.widget.RecyclerView; + +public class ListViewHolder extends RecyclerView.ViewHolder { + public ListViewHolder(T v) { + super(v); + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/helpers/AlphaSpan.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/helpers/AlphaSpan.java new file mode 100644 index 000000000..9839df1fa --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/helpers/AlphaSpan.java @@ -0,0 +1,24 @@ +package com.skywire.skycoin.vpn.helpers; + +import android.text.TextPaint; +import android.text.style.TypefaceSpan; + +public class AlphaSpan extends TypefaceSpan { + private int alpha; + + public AlphaSpan(int alpha) { + super(""); + + this.alpha = alpha; + } + + @Override + public void updateDrawState(TextPaint paint) { + paint.setAlpha(alpha); + } + + @Override + public void updateMeasureState(TextPaint paint) { + paint.setAlpha(alpha); + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/helpers/BoxRowTypes.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/helpers/BoxRowTypes.java new file mode 100644 index 000000000..b4b9339fa --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/helpers/BoxRowTypes.java @@ -0,0 +1,8 @@ +package com.skywire.skycoin.vpn.helpers; + +public enum BoxRowTypes { + TOP, + MIDDLE, + BOTTOM, + SINGLE, +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/helpers/ClickTimeManagement.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/helpers/ClickTimeManagement.java new file mode 100644 index 000000000..a8ffd434e --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/helpers/ClickTimeManagement.java @@ -0,0 +1,40 @@ +package com.skywire.skycoin.vpn.helpers; + +import java.util.concurrent.TimeUnit; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public class ClickTimeManagement { + public static final int normalFastClickPreventionDelay = 700; + + private Disposable timeSubscription; + private int delay = normalFastClickPreventionDelay; + + public void setDelay(int delay) { + this.delay = delay; + } + + public void informClickMade() { + removeDelay(); + + timeSubscription = Observable.just(1).delay(delay, TimeUnit.MILLISECONDS) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(v -> timeSubscription = null); + } + + public boolean canClick() { + return timeSubscription == null; + } + + public void removeDelay() { + if (timeSubscription != null) { + timeSubscription.dispose(); + } + + timeSubscription = null; + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/helpers/CountriesList.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/helpers/CountriesList.java new file mode 100644 index 000000000..285b53ad4 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/helpers/CountriesList.java @@ -0,0 +1,267 @@ +package com.skywire.skycoin.vpn.helpers; + +import com.skywire.skycoin.vpn.App; +import com.skywire.skycoin.vpn.R; + +import java.util.HashMap; + +public class CountriesList { + private static HashMap countries = new HashMap() {{ + put("AF", "Afghanistan"); + put("AX", "Aland Islands"); + put("AL", "Albania"); + put("DZ", "Algeria"); + put("AS", "American Samoa"); + put("AD", "Andorra"); + put("AO", "Angola"); + put("AI", "Anguilla"); + put("AQ", "Antarctica"); + put("AG", "Antigua and Barbuda"); + put("AR", "Argentina"); + put("AM", "Armenia"); + put("AW", "Aruba"); + put("AU", "Australia"); + put("AT", "Austria"); + put("AZ", "Azerbaijan"); + put("BS", "Bahamas"); + put("BH", "Bahrain"); + put("BD", "Bangladesh"); + put("BB", "Barbados"); + put("BY", "Belarus"); + put("BE", "Belgium"); + put("BZ", "Belize"); + put("BJ", "Benin"); + put("BM", "Bermuda"); + put("BT", "Bhutan"); + put("BO", "Bolivia"); + put("BA", "Bosnia and Herzegovina"); + put("BW", "Botswana"); + put("BV", "Bouvet Island"); + put("BR", "Brazil"); + put("IO", "British Indian Ocean Territory"); + put("BN", "Brunei Darussalam"); + put("BG", "Bulgaria"); + put("BF", "Burkina Faso"); + put("BI", "Burundi"); + put("KH", "Cambodia"); + put("CM", "Cameroon"); + put("CA", "Canada"); + put("CV", "Cape Verde"); + put("KY", "Cayman Islands"); + put("CF", "Central African Republic"); + put("TD", "Chad"); + put("CL", "Chile"); + put("CN", "China"); + put("CX", "Christmas Island"); + put("CC", "Cocos (Keeling) Islands"); + put("CO", "Colombia"); + put("KM", "Comoros"); + put("CG", "Congo"); + put("CD", "Congo, Democratic Republic"); + put("CK", "Cook Islands"); + put("CR", "Costa Rica"); + put("CI", "Cote D'Ivoire"); + put("HR", "Croatia"); + put("CU", "Cuba"); + put("CY", "Cyprus"); + put("CZ", "Czech Republic"); + put("DK", "Denmark"); + put("DJ", "Djibouti"); + put("DM", "Dominica"); + put("DO", "Dominican Republic"); + put("EC", "Ecuador"); + put("EG", "Egypt"); + put("SV", "El Salvador"); + put("GQ", "Equatorial Guinea"); + put("ER", "Eritrea"); + put("EE", "Estonia"); + put("ET", "Ethiopia"); + put("FK", "Falkland Islands (Malvinas)"); + put("FO", "Faroe Islands"); + put("FJ", "Fiji"); + put("FI", "Finland"); + put("FR", "France"); + put("GF", "French Guiana"); + put("PF", "French Polynesia"); + put("TF", "French Southern Territories"); + put("GA", "Gabon"); + put("GM", "Gambia"); + put("GE", "Georgia"); + put("DE", "Germany"); + put("GH", "Ghana"); + put("GI", "Gibraltar"); + put("GR", "Greece"); + put("GL", "Greenland"); + put("GD", "Grenada"); + put("GP", "Guadeloupe"); + put("GU", "Guam"); + put("GT", "Guatemala"); + put("GG", "Guernsey"); + put("GN", "Guinea"); + put("GW", "Guinea-Bissau"); + put("GY", "Guyana"); + put("HT", "Haiti"); + put("HM", "Heard Island and Mcdonald Islands"); + put("VA", "Holy See (Vatican City State)"); + put("HN", "Honduras"); + put("HK", "Hong Kong"); + put("HU", "Hungary"); + put("IS", "Iceland"); + put("IN", "India"); + put("ID", "Indonesia"); + put("IR", "Iran"); + put("IQ", "Iraq"); + put("IE", "Ireland"); + put("IM", "Isle of Man"); + put("IL", "Israel"); + put("IT", "Italy"); + put("JM", "Jamaica"); + put("JP", "Japan"); + put("JE", "Jersey"); + put("JO", "Jordan"); + put("KZ", "Kazakhstan"); + put("KE", "Kenya"); + put("KI", "Kiribati"); + put("KP", "Korea (North)"); + put("KR", "Korea (South)"); + put("XK", "Kosovo"); + put("KW", "Kuwait"); + put("KG", "Kyrgyzstan"); + put("LA", "Laos"); + put("LV", "Latvia"); + put("LB", "Lebanon"); + put("LS", "Lesotho"); + put("LR", "Liberia"); + put("LY", "Libyan Arab Jamahiriya"); + put("LI", "Liechtenstein"); + put("LT", "Lithuania"); + put("LU", "Luxembourg"); + put("MO", "Macao"); + put("MK", "Macedonia"); + put("MG", "Madagascar"); + put("MW", "Malawi"); + put("MY", "Malaysia"); + put("MV", "Maldives"); + put("ML", "Mali"); + put("MT", "Malta"); + put("MH", "Marshall Islands"); + put("MQ", "Martinique"); + put("MR", "Mauritania"); + put("MU", "Mauritius"); + put("YT", "Mayotte"); + put("MX", "Mexico"); + put("FM", "Micronesia"); + put("MD", "Moldova"); + put("MC", "Monaco"); + put("MN", "Mongolia"); + put("MS", "Montserrat"); + put("MA", "Morocco"); + put("MZ", "Mozambique"); + put("MM", "Myanmar"); + put("NA", "Namibia"); + put("NR", "Nauru"); + put("NP", "Nepal"); + put("NL", "Netherlands"); + put("AN", "Netherlands Antilles"); + put("NC", "New Caledonia"); + put("NZ", "New Zealand"); + put("NI", "Nicaragua"); + put("NE", "Niger"); + put("NG", "Nigeria"); + put("NU", "Niue"); + put("NF", "Norfolk Island"); + put("MP", "Northern Mariana Islands"); + put("NO", "Norway"); + put("OM", "Oman"); + put("PK", "Pakistan"); + put("PW", "Palau"); + put("PS", "Palestinian Territory, Occupied"); + put("PA", "Panama"); + put("PG", "Papua New Guinea"); + put("PY", "Paraguay"); + put("PE", "Peru"); + put("PH", "Philippines"); + put("PN", "Pitcairn"); + put("PL", "Poland"); + put("PT", "Portugal"); + put("PR", "Puerto Rico"); + put("QA", "Qatar"); + put("RE", "Reunion"); + put("RO", "Romania"); + put("RU", "Russian Federation"); + put("RW", "Rwanda"); + put("SH", "Saint Helena"); + put("KN", "Saint Kitts and Nevis"); + put("LC", "Saint Lucia"); + put("PM", "Saint Pierre and Miquelon"); + put("VC", "Saint Vincent and the Grenadines"); + put("WS", "Samoa"); + put("SM", "San Marino"); + put("ST", "Sao Tome and Principe"); + put("SA", "Saudi Arabia"); + put("SN", "Senegal"); + put("RS", "Serbia"); + put("ME", "Montenegro"); + put("SC", "Seychelles"); + put("SL", "Sierra Leone"); + put("SG", "Singapore"); + put("SK", "Slovakia"); + put("SI", "Slovenia"); + put("SB", "Solomon Islands"); + put("SO", "Somalia"); + put("ZA", "South Africa"); + put("GS", "South Georgia and the South Sandwich Islands"); + put("ES", "Spain"); + put("LK", "Sri Lanka"); + put("SD", "Sudan"); + put("SR", "Suriname"); + put("SJ", "Svalbard and Jan Mayen"); + put("SZ", "Swaziland"); + put("SE", "Sweden"); + put("CH", "Switzerland"); + put("SY", "Syrian Arab Republic"); + put("TW", "Taiwan, Province of China"); + put("TJ", "Tajikistan"); + put("TZ", "Tanzania"); + put("TH", "Thailand"); + put("TL", "Timor-Leste"); + put("TG", "Togo"); + put("TK", "Tokelau"); + put("TO", "Tonga"); + put("TT", "Trinidad and Tobago"); + put("TN", "Tunisia"); + put("TR", "Turkey"); + put("TM", "Turkmenistan"); + put("TC", "Turks and Caicos Islands"); + put("TV", "Tuvalu"); + put("UG", "Uganda"); + put("UA", "Ukraine"); + put("AE", "United Arab Emirates"); + put("GB", "United Kingdom"); + put("US", "United States"); + put("UM", "United States Minor Outlying Islands"); + put("UY", "Uruguay"); + put("UZ", "Uzbekistan"); + put("VU", "Vanuatu"); + put("VE", "Venezuela"); + put("VN", "Viet Nam"); + put("VG", "Virgin Islands, British"); + put("VI", "Virgin Islands, U.S."); + put("WF", "Wallis and Futuna"); + put("EH", "Western Sahara"); + put("YE", "Yemen"); + put("ZM", "Zambia"); + put("ZW", "Zimbabwe"); + put("ZZ", "Unknown"); + }}; + + public static String getCountryName(String cuntryCode) { + cuntryCode = cuntryCode.toUpperCase(); + + if (!cuntryCode.equals("ZZ") && countries.containsKey(cuntryCode)) { + return countries.get(cuntryCode); + } + + return App.getContext().getText(R.string.general_unknown).toString(); + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/helpers/Globals.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/helpers/Globals.java new file mode 100644 index 000000000..d1942445a --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/helpers/Globals.java @@ -0,0 +1,70 @@ +package com.skywire.skycoin.vpn.helpers; + +import androidx.annotation.NonNull; + +/** + * Constant values used in various parts of the app. + */ +public class Globals { + /** + * Time to wait before sending a click event after the user clicks a button. This is for + * allowing the UI to show the click effect. + */ + public static final int CLICK_DELAY_MS = 150; + /** + * Address of the local Skywire node. + */ + public static final String LOCAL_VISOR_ADDRESS = "localhost"; + /** + * Port of the local Skywire node. + */ + public static final int LOCAL_VISOR_PORT = 7890; + + /** + * Addresses used for checking if the device has internet connectivity. Any number of + * addresses, but at least 1, can be used. Addresses will be checked sequentially and only + * until being able to connect with one. + */ + public static final String[] INTERNET_CHECKING_ADDRESSES = new String[]{"https://dmsg.discovery.skywire.skycoin.com", "https://www.skycoin.com"}; + + /** + * Options for how to show the VPN data transmission stats. + */ + public enum DataUnits { + BitsSpeedAndBytesVolume, + OnlyBytes, + OnlyBits, + } + + /** + * List with all the possible app selection modes. Each option has an associated string value. + */ + public enum AppFilteringModes { + /** + * All apps must be protected by the VPN service, no matter which apps have been selected + * by the user. + */ + PROTECT_ALL("PROTECT_ALL"), + /** + * Only the apps selected by the user must be protected by the VPN service. + */ + PROTECT_SELECTED("PROTECT_SELECTED"), + /** + * Apps selected by the user must NOT be protected by the VPN service. All other apps + * must be protected. + */ + IGNORE_SELECTED("IGNORE_SELECTED"); + + private final String val; + + AppFilteringModes(final String val) { + this.val = val; + } + + @NonNull + @Override + public String toString() { + return val; + } + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/helpers/HelperFunctions.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/helpers/HelperFunctions.java new file mode 100644 index 000000000..21e3bcb78 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/helpers/HelperFunctions.java @@ -0,0 +1,662 @@ +package com.skywire.skycoin.vpn.helpers; + +import android.app.Activity; +import android.app.Dialog; +import android.app.PendingIntent; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.os.Handler; +import android.os.Looper; +import android.util.TypedValue; +import android.view.Window; +import android.view.WindowManager; +import android.widget.Toast; + +import androidx.core.content.ContextCompat; + +import com.skywire.skycoin.vpn.App; +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.activities.main.MainActivity; +import com.skywire.skycoin.vpn.activities.servers.ServerLists; +import com.skywire.skycoin.vpn.activities.servers.VpnServerForList; +import com.skywire.skycoin.vpn.controls.ConfirmationModalWindow; +import com.skywire.skycoin.vpn.controls.EditServerValueModalWindow; +import com.skywire.skycoin.vpn.controls.ServerInfoModalWindow; +import com.skywire.skycoin.vpn.controls.ServerPasswordModalWindow; +import com.skywire.skycoin.vpn.controls.options.OptionsItem; +import com.skywire.skycoin.vpn.controls.options.OptionsModalWindow; +import com.skywire.skycoin.vpn.network.ApiClient; +import com.skywire.skycoin.vpn.objects.LocalServerData; +import com.skywire.skycoin.vpn.objects.ServerFlags; +import com.skywire.skycoin.vpn.vpn.VPNCoordinator; +import com.skywire.skycoin.vpn.vpn.VPNGeneralPersistentData; +import com.skywire.skycoin.vpn.vpn.VPNServersPersistentData; + +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +import io.reactivex.rxjava3.core.Observable; +import skywiremob.Skywiremob; + +/** + * General helper functions for different parts of the app. + */ +public class HelperFunctions { + public enum WidthTypes { + SMALL, + BIG, + BIGGER, + } + + // Helpers for showing only a max number of decimals. + public static final DecimalFormat twoDecimalsFormatter = new DecimalFormat("#.##"); + public static final DecimalFormat oneDecimalsFormatter = new DecimalFormat("#.#"); + public static final DecimalFormat zeroDecimalsFormatter = new DecimalFormat("#"); + + // Last toast notification shown. + private static Toast lastToast; + + /** + * Displays debug information about an error in the console. It includes the several details. + * @param prefix Text to show before the error details. + * @param e Error. + */ + public static void logError(String prefix, Throwable e) { + // Print the basic error msgs. + StringBuilder errorMsg = new StringBuilder(prefix + ": " + e.getMessage() + "\n"); + errorMsg.append(e.toString()).append("\n"); + + // Print the stack. + StackTraceElement[] stackTrace = e.getStackTrace(); + for (StackTraceElement stackTraceElement : stackTrace) { + errorMsg.append(stackTraceElement.toString()).append("\n"); + } + + // Display in the console. + Skywiremob.printString(errorMsg.toString()); + } + + /** + * Displays an error msg in the console. + * @param prefix Text to show before the error msg. + * @param errorText Error msg. + */ + public static void logError(String prefix, String errorText) { + String errorMsg = prefix + ": " + errorText; + Skywiremob.printString(errorMsg); + } + + /** + * Shows a toast notification. Can be used from background threads. + * @param text Text for the notification. + * @param shortDuration If the duration of the notification must be short (true) or + * long (false). + */ + public static void showToast(String text, boolean shortDuration) { + // Run in the UI thread. + Handler handler = new Handler(Looper.getMainLooper()); + handler.post(() -> { + // Close the previous notification. + if (lastToast != null) { + lastToast.cancel(); + } + + // Show the notification. + lastToast = Toast.makeText(App.getContext(), text, shortDuration ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG); + lastToast.show(); + }); + } + + /** + * Gets the list of the app launchers installed in the device. More than one entry may share + * the same package name. The current app is ignored. + */ + public static List getDeviceAppsList() { + Intent mainIntent = new Intent(Intent.ACTION_MAIN, null); + mainIntent.addCategory(Intent.CATEGORY_LAUNCHER); + + String packageName = App.getContext().getPackageName(); + ArrayList response = new ArrayList<>(); + + // Get all the entries in the device which coincide with the intent. + for (ResolveInfo app : App.getContext().getPackageManager().queryIntentActivities( mainIntent, 0)) { + if (!app.activityInfo.packageName.equals(packageName)) { + response.add(app); + } + } + + return response; + } + + /** + * Filters a list of package names and returns only the ones which are from launchers + * currently installed in the device. The current app is ignored. + * @param apps List to filter. + * @return Filtered list. + */ + public static HashSet filterAvailableApps(HashSet apps) { + HashSet availableApps = new HashSet<>(); + for (ResolveInfo app : getDeviceAppsList()) { + availableApps.add(app.activityInfo.packageName); + } + + HashSet response = new HashSet<>(); + for (String app : apps) { + if (availableApps.contains(app)) { + response.add(app); + } + } + + return response; + } + + /** + * Closes the provided activity if the VPN service is running. If the activity is closed, + * a toast is shown. + * @param activity Activity to close. + * @return True if the activity was closed, false if not. + */ + public static boolean closeActivityIfServiceRunning(Activity activity) { + if (VPNCoordinator.getInstance().isServiceRunning()) { + HelperFunctions.showToast(App.getContext().getString(R.string.vpn_already_running_warning), true); + activity.finish(); + + return true; + } + + return false; + } + + /** + * Checks if there is connection via internet to at least one of the testing URLs set in the + * globals class. + * @param logError If true and there is an error checking the connection, the error will + * be logged. + * @return Observable which emits if there is connection or not. + */ + public static Observable checkInternetConnectivity(boolean logError) { + return checkInternetConnectivity(0, logError); + } + + /** + * Internal function for checking if there is internet connectivity, recursively. + * @param urlIndex Index of the testing URL to check. + * @param logError If the error, if any, must be logged at the end of the operation. + */ + private static Observable checkInternetConnectivity(int urlIndex, boolean logError) { + return ApiClient.checkConnection(Globals.INTERNET_CHECKING_ADDRESSES[urlIndex]) + // If there is a valid response, return true. + .map(response -> true) + .onErrorResumeNext(err -> { + // If there is an error and there are more testing URLs, continue to the next step. + if (urlIndex < Globals.INTERNET_CHECKING_ADDRESSES.length - 1) { + return checkInternetConnectivity(urlIndex + 1, logError); + } + + if (logError) { + HelperFunctions.logError("Checking network connectivity", err); + } + + return Observable.just(false); + }); + } + + /** + * Returns an intent for opening the app. + */ + public static PendingIntent getOpenAppPendingIntent() { + final Intent openAppIntent = new Intent(App.getContext(), MainActivity.class); + openAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + openAppIntent.setAction(Intent.ACTION_MAIN); + openAppIntent.addCategory(Intent.CATEGORY_LAUNCHER); + + return PendingIntent.getActivity(App.getContext(), 0, openAppIntent, PendingIntent.FLAG_UPDATE_CURRENT); + } + + /** + * Allows to convert a bytes value to KB, MB, GB, etc. It considers 1024, and not 1000, a K. + * @param bytes Amount of data to process, in bytes. + * @param calculatePerSecond If true, the result will have "/s" added at the end. + * @param useBits If the data must be shown in bits (true) or bytes (false). + */ + public static String computeDataAmountString(double bytes, boolean calculatePerSecond, boolean useBits) { + double current = (double)bytes; + + // Set the correct units. + String[] scales; + if (calculatePerSecond) { + if (useBits) { + scales = new String[]{" b/s", " Kb/s", " Mb/s", " Gb/s", " Tb/s", "Pb/s", "Eb/s", "Zb/s", "Yb/s"}; + } else { + scales = new String[]{" B/s", " KB/s", " MB/s", " GB/s", " TB/s", "PB/s", "EB/s", "ZB/s", "YB/s"}; + } + } else { + if (useBits) { + scales = new String[]{" b", " Kb", " Mb", " Gb", " Tb", "Pb", "Eb", "Zb", "Yb"}; + } else { + scales = new String[]{" B", " KB", " MB", " GB", " TB", "PB", "EB", "ZB", "YB"}; + } + } + + // Convert to bits, if needed. + if (useBits) { + current *= 8; + } + + // Divide the speed by 1024 until getting an appropriate scale to return. + for (int i = 0; i < scales.length - 1; i++) { + if (current < 1024) { + // Return decimals depending on how long the number is. + if (current < 10) { + return twoDecimalsFormatter.format(current) + scales[i]; + } else if (current < 100) { + return oneDecimalsFormatter.format(current) + scales[i]; + } + + return zeroDecimalsFormatter.format(current) + scales[i]; + } + + current /= 1024; + } + + return current + scales[scales.length - 1]; + } + + public static String getLatencyValue(double latency) { + String initialPart; + String lastPart; + + if (latency >= 1000) { + initialPart = oneDecimalsFormatter.format(latency / 1000); + lastPart = App.getContext().getString(R.string.general_seconds_abbreviation); + } else { + initialPart = oneDecimalsFormatter.format(latency); + lastPart = App.getContext().getString(R.string.general_milliseconds_abbreviation); + } + + return initialPart + lastPart; + } + + public static int getFlagResourceId(String countryCode) { + if (countryCode.toLowerCase() != "do") { + int flagResourceId = App.getContext().getResources().getIdentifier( + countryCode.toLowerCase(), + "drawable", + App.getContext().getPackageName() + ); + + if (flagResourceId != 0) { + return flagResourceId; + } else { + return R.drawable.zz; + } + } else { + return R.drawable.do_flag; + } + } + + // TODO: for currently commented fields, must be deleted or reactivated depending on what happens to the fields. + /* + public static int getCongestionNumberColor(int congestion) { + if (congestion < 60) { + return ContextCompat.getColor(App.getContext(), R.color.green); + } else if (congestion < 90) { + return ContextCompat.getColor(App.getContext(), R.color.yellow); + } + + return ContextCompat.getColor(App.getContext(), R.color.red); + } + + public static int getLatencyNumberColor(int latency) { + if (latency < 200) { + return ContextCompat.getColor(App.getContext(), R.color.green); + } else if (latency < 350) { + return ContextCompat.getColor(App.getContext(), R.color.yellow); + } + + return ContextCompat.getColor(App.getContext(), R.color.red); + } + + public static int getHopsNumberColor(int hops) { + if (hops < 5) { + return ContextCompat.getColor(App.getContext(), R.color.green); + } else if (hops < 9) { + return ContextCompat.getColor(App.getContext(), R.color.yellow); + } + + return ContextCompat.getColor(App.getContext(), R.color.red); + } + */ + public static void configureModalWindow(Dialog modal) { + Window window = modal.getWindow(); + window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); + + WidthTypes screenWidthType = getWidthType(modal.getContext()); + if (screenWidthType != WidthTypes.SMALL) { + int width = (int)TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 500, + modal.getContext().getResources().getDisplayMetrics() + ); + + WindowManager.LayoutParams params = window.getAttributes(); + params.width = width; + params.height = WindowManager.LayoutParams.WRAP_CONTENT; + window.setAttributes(params); + } + } + + public static boolean showBackgroundForVerticalScreen() { + double proportion = (double)Resources.getSystem().getDisplayMetrics().widthPixels / (double)Resources.getSystem().getDisplayMetrics().heightPixels; + if (proportion > 1.1) { + return false; + } + + return true; + } + + public static WidthTypes getWidthType(Context ctx) { + int screenWidthInDP = (int)(Resources.getSystem().getDisplayMetrics().widthPixels / ctx.getResources().getDisplayMetrics().density); + + if (screenWidthInDP >= 1100) { + return WidthTypes.BIGGER; + } else if (screenWidthInDP >= 800) { + return WidthTypes.BIG; + } + + return WidthTypes.SMALL; + } + + public static int getTabletExtraHorizontalPadding(Context ctx) { + WidthTypes widthType = getWidthType(ctx); + + if (widthType == WidthTypes.BIGGER) { + return (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 100, + ctx.getResources().getDisplayMetrics() + ); + } else if (widthType == WidthTypes.BIG) { + return (int)TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 40, + ctx.getResources().getDisplayMetrics() + ); + } + + return 0; + } + + public static boolean prepareAndStartVpn(Activity requestingActivity, LocalServerData server) { + if (server.flag == ServerFlags.Blocked) { + HelperFunctions.showToast(requestingActivity.getString(R.string.general_starting_blocked_server_error) + server.pk, false); + + return false; + } + + long err = Skywiremob.isPKValid(server.pk).getCode(); + if (err != Skywiremob.ErrCodeNoError) { + HelperFunctions.showToast(requestingActivity.getString(R.string.vpn_coordinator_invalid_credentials_error) + server.pk, false); + return false; + } else { + Skywiremob.printString("PK is correct"); + } + + Globals.AppFilteringModes selectedMode = VPNGeneralPersistentData.getAppsSelectionMode(); + if (selectedMode != Globals.AppFilteringModes.PROTECT_ALL) { + HashSet selectedApps = HelperFunctions.filterAvailableApps(VPNGeneralPersistentData.getAppList(new HashSet<>())); + + if (selectedApps.size() == 0) { + if (selectedMode == Globals.AppFilteringModes.PROTECT_SELECTED) { + HelperFunctions.showToast(requestingActivity.getString(R.string.vpn_no_apps_to_protect_warning), false); + } else { + HelperFunctions.showToast(requestingActivity.getString(R.string.vpn_no_apps_to_ignore_warning), false); + } + } + } + + VPNCoordinator.getInstance().startVPN( + requestingActivity, + server + ); + + return true; + } + + public static String getServerName(VpnServerForList server, String defaultName) { + if ((server.name == null || server.name.trim().equals("")) && (server.customName == null || server.customName.trim().equals(""))) { + return defaultName; + } else if (server.name != null && !server.name.trim().equals("") && (server.customName == null || server.customName.trim().equals(""))) { + return server.name; + } else if (server.customName != null && !server.customName.trim().equals("") && (server.name == null || server.name.trim().equals(""))) { + return server.customName; + } + + return server.customName + " - " + server.name; + } + + public static String getServerNote(LocalServerData server) { + String note = ""; + if (server.note != null && !server.note.trim().equals("")) { + note = server.note; + } + if (server.personalNote != null && !server.personalNote.trim().equals("")) { + if (note.length() > 0) { + note += " - "; + } + note += server.personalNote; + } + + return note.length() > 0 ? note : null; + } + + public static void showServerOptions(Context ctx, VpnServerForList server, ServerLists listType) { + ArrayList options = new ArrayList(); + ArrayList optionCodes = new ArrayList(); + + OptionsItem.SelectableOption option = new OptionsItem.SelectableOption(); + option.icon = "\ue88e"; + option.translatableLabelId = R.string.tmp_server_options_view_info; + options.add(option); + optionCodes.add(10); + option = new OptionsItem.SelectableOption(); + option.icon = "\ue14d"; + option.translatableLabelId = R.string.tmp_server_options_copy_pk; + options.add(option); + optionCodes.add(11); + option = new OptionsItem.SelectableOption(); + option.icon = "\ue3c9"; + option.translatableLabelId = R.string.tmp_server_options_name; + options.add(option); + optionCodes.add(101); + option = new OptionsItem.SelectableOption(); + option.icon = "\ue8d2"; + option.translatableLabelId = R.string.tmp_server_options_note; + options.add(option); + optionCodes.add(102); + + if (server.hasPassword) { + option = new OptionsItem.SelectableOption(); + option.icon = "\ue898"; + option.translatableLabelId = R.string.tmp_server_options_remove_password; + options.add(option); + optionCodes.add(201); + + option = new OptionsItem.SelectableOption(); + option.icon = "\ue899"; + option.translatableLabelId = R.string.tmp_server_options_change_password; + options.add(option); + optionCodes.add(202); + } else { + if (server.enteredManually) { + option = new OptionsItem.SelectableOption(); + option.icon = "\ue899"; + option.translatableLabelId = R.string.tmp_server_options_add_password; + options.add(option); + optionCodes.add(202); + } + } + + if (server.flag != ServerFlags.Favorite) { + option = new OptionsItem.SelectableOption(); + option.icon = "\ue838"; + option.translatableLabelId = R.string.tmp_server_options_make_favorite; + options.add(option); + optionCodes.add(1); + } + + if (server.flag == ServerFlags.Favorite) { + option = new OptionsItem.SelectableOption(); + option.icon = "\ue83a"; + option.translatableLabelId = R.string.tmp_server_options_remove_from_favorites; + options.add(option); + optionCodes.add(-1); + } + + if (server.flag != ServerFlags.Blocked) { + option = new OptionsItem.SelectableOption(); + option.icon = "\ue925"; + option.translatableLabelId = R.string.tmp_server_options_block; + options.add(option); + optionCodes.add(2); + } + + if (server.flag == ServerFlags.Blocked) { + option = new OptionsItem.SelectableOption(); + option.icon = "\ue8dc"; + option.translatableLabelId = R.string.tmp_server_options_unblock; + options.add(option); + optionCodes.add(-2); + } + + if (server.inHistory) { + option = new OptionsItem.SelectableOption(); + option.icon = "\ue872"; + option.translatableLabelId = R.string.tmp_server_options_remove_from_history; + options.add(option); + optionCodes.add(-3); + } + + OptionsModalWindow modal = new OptionsModalWindow(ctx, null, options, (int selectedOption) -> { + LocalServerData savedVersion_ = VPNServersPersistentData.getInstance().getSavedVersion(server.pk); + if (savedVersion_ == null) { + savedVersion_ = VPNServersPersistentData.getInstance().processFromList(server); + } + + final LocalServerData savedVersion = savedVersion_; + + if (optionCodes.get(selectedOption) > 200) { + if (VPNCoordinator.getInstance().isServiceRunning() && VPNServersPersistentData.getInstance().getCurrentServer().pk.equals(savedVersion.pk)) { + HelperFunctions.showToast(App.getContext().getText(R.string.general_server_running_error).toString(), true); + return; + } + + if (optionCodes.get(selectedOption) == 201) { + ConfirmationModalWindow confirmationModal = new ConfirmationModalWindow( + ctx, + R.string.tmp_server_options_remove_password_confirmation, + R.string.tmp_confirmation_yes, + R.string.tmp_confirmation_no, + () -> { + VPNServersPersistentData.getInstance().removePassword(savedVersion.pk); + HelperFunctions.showToast(ctx.getString(R.string.tmp_server_options_remove_password_done), true); + } + ); + confirmationModal.show(); + } else { + ServerPasswordModalWindow passwordModal = new ServerPasswordModalWindow( + ctx, + server + ); + passwordModal.show(); + } + } else if (optionCodes.get(selectedOption) > 100) { + EditServerValueModalWindow valueModal = new EditServerValueModalWindow( + ctx, + optionCodes.get(selectedOption) == 101, + server + ); + valueModal.show(); + } else if (optionCodes.get(selectedOption) == 10) { + ServerInfoModalWindow infoModal = new ServerInfoModalWindow(ctx, server, listType); + infoModal.show(); + } else if (optionCodes.get(selectedOption) == 11) { + ClipboardManager clipboard = (ClipboardManager)ctx.getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clipData = ClipData.newPlainText("", server.pk); + clipboard.setPrimaryClip(clipData); + HelperFunctions.showToast(ctx.getString(R.string.general_copied), true); + } else if (optionCodes.get(selectedOption) == 1) { + if (server.flag != ServerFlags.Blocked) { + VPNServersPersistentData.getInstance().changeFlag(savedVersion, ServerFlags.Favorite); + HelperFunctions.showToast(ctx.getString(R.string.tmp_server_options_make_favorite_done), true); + return; + } + + ConfirmationModalWindow confirmationModal = new ConfirmationModalWindow( + ctx, + R.string.tmp_server_options_make_favorite_from_blocked_confirmation, + R.string.tmp_confirmation_yes, + R.string.tmp_confirmation_no, + () -> { + VPNServersPersistentData.getInstance().changeFlag(savedVersion, ServerFlags.Favorite); + HelperFunctions.showToast(ctx.getString(R.string.tmp_server_options_make_favorite_done), true); + } + ); + confirmationModal.show(); + } else if (optionCodes.get(selectedOption) == -1) { + VPNServersPersistentData.getInstance().changeFlag(savedVersion, ServerFlags.None); + HelperFunctions.showToast(ctx.getString(R.string.tmp_server_options_remove_from_favorites_done), true); + } else if (optionCodes.get(selectedOption) == 2) { + if (VPNServersPersistentData.getInstance().getCurrentServer() != null && + VPNServersPersistentData.getInstance().getCurrentServer().pk.toLowerCase().equals(server.pk.toLowerCase()) + ) { + HelperFunctions.showToast(ctx.getString(R.string.tmp_server_options_block_error), true); + return; + } + + if (server.flag != ServerFlags.Favorite) { + VPNServersPersistentData.getInstance().changeFlag(savedVersion, ServerFlags.Blocked); + HelperFunctions.showToast(ctx.getString(R.string.tmp_server_options_block_done), true); + return; + } + + ConfirmationModalWindow confirmationModal = new ConfirmationModalWindow( + ctx, + R.string.tmp_server_options_block_favorite_confirmation, + R.string.tmp_confirmation_yes, + R.string.tmp_confirmation_no, + () -> { + VPNServersPersistentData.getInstance().changeFlag(savedVersion, ServerFlags.Blocked); + HelperFunctions.showToast(ctx.getString(R.string.tmp_server_options_block_done), true); + } + ); + confirmationModal.show(); + } else if (optionCodes.get(selectedOption) == -2) { + VPNServersPersistentData.getInstance().changeFlag(savedVersion, ServerFlags.None); + HelperFunctions.showToast(ctx.getString(R.string.tmp_server_options_unblock_done), true); + } else if (optionCodes.get(selectedOption) == -3) { + ConfirmationModalWindow confirmationModal = new ConfirmationModalWindow( + ctx, + R.string.tmp_server_options_remove_from_history_confirmation, + R.string.tmp_confirmation_yes, + R.string.tmp_confirmation_no, + () -> { + VPNServersPersistentData.getInstance().removeFromHistory(savedVersion.pk); + HelperFunctions.showToast(ctx.getString(R.string.tmp_server_options_remove_from_history_done), true); + } + ); + confirmationModal.show(); + } + }); + modal.show(); + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/helpers/MaterialFontSpan.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/helpers/MaterialFontSpan.java new file mode 100644 index 000000000..f4d5f6ce1 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/helpers/MaterialFontSpan.java @@ -0,0 +1,32 @@ +package com.skywire.skycoin.vpn.helpers; + +import android.content.Context; +import android.graphics.Typeface; +import android.text.TextPaint; +import android.text.style.TypefaceSpan; + +import androidx.core.content.res.ResourcesCompat; + +import com.skywire.skycoin.vpn.R; + +public class MaterialFontSpan extends TypefaceSpan { + private static Typeface materialFont; + + public MaterialFontSpan(Context context) { + super(""); + + if (materialFont == null) { + materialFont = ResourcesCompat.getFont(context, R.font.material_font); + } + } + + @Override + public void updateDrawState(TextPaint paint) { + paint.setTypeface(materialFont); + } + + @Override + public void updateMeasureState(TextPaint paint) { + paint.setTypeface(materialFont); + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/helpers/Notifications.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/helpers/Notifications.java new file mode 100644 index 000000000..a0e7501dc --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/helpers/Notifications.java @@ -0,0 +1,162 @@ +package com.skywire.skycoin.vpn.helpers; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; + +import androidx.core.app.NotificationCompat; + +import com.skywire.skycoin.vpn.App; +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.vpn.VPNGeneralPersistentData; +import com.skywire.skycoin.vpn.vpn.VPNStates; + +import io.reactivex.rxjava3.disposables.Disposable; +import skywiremob.Skywiremob; + +/** + * Constant values and helper functions for showing notifications. + */ +public class Notifications { + /** + * ID of the notification channel for showing the VPN service status. + */ + public static final String NOTIFICATION_CHANNEL_ID = "SkywireVPN"; + /** + * ID of the notification channel for showing alerts and errors. + */ + public static final String ALERT_NOTIFICATION_CHANNEL_ID = "SkywireVPNAlerts"; + + /** + * ID of the VPN service status notification. + */ + public static final int SERVICE_STATUS_NOTIFICATION_ID = 1; + /** + * ID of the notification for informing about errors while trying to automatically start the + * VPN service during boot. + */ + public static final int AUTOSTART_ALERT_NOTIFICATION_ID = 10; + /** + * ID of the generic error notifications. + */ + public static final int ERROR_NOTIFICATION_ID = 50; + + /** + * Units used for showing the data transmission stats. + */ + private static Globals.DataUnits dataUnits = VPNGeneralPersistentData.getDataUnits(); + /** + * Subscription for updating the data transmission stats. + */ + private static Disposable dataUnitsSubscription; + + /** + * Closes all the alert and error notifications created by the app. Only notifications with + * the IDs defined in this class will be closed. + */ + public static void removeAllAlertNotifications() { + NotificationManager notificationManager = (NotificationManager) App.getContext().getSystemService(Context.NOTIFICATION_SERVICE); + + notificationManager.cancel(AUTOSTART_ALERT_NOTIFICATION_ID); + notificationManager.cancel(ERROR_NOTIFICATION_ID); + } + + /** + * Creates and shows an alert notification. + * @param ID Notification ID. Please use one of the IDs defined in this class. + * @param title Notification title. + * @param content Main notification text. + * @param contentIntent Intent for when the user presses the notification. + */ + public static void showAlertNotification(int ID, String title, String content, PendingIntent contentIntent) { + // Create the style for a multiline notification. It will be ignore if the OS does not + // support it. + NotificationCompat.BigTextStyle bigTextStyle = new NotificationCompat.BigTextStyle() + .setBigContentTitle(title) + .bigText(content); + + // Create the notification. + Notification notification = new NotificationCompat.Builder(App.getContext(), ALERT_NOTIFICATION_CHANNEL_ID) + .setSmallIcon(R.drawable.ic_error) + .setContentTitle(title) + .setContentText(content) + .setStyle(bigTextStyle) + .setContentIntent(contentIntent) + .build(); + + // Show it. + NotificationManager notificationManager = (NotificationManager)App.getContext().getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.notify(ID, notification); + } + + /** + * Creates a notification for displaying the current state of the VPN service. The notification + * is returned, not displayed. + * @param currentState Current state of the VPN service. + * @param protectionEnabled If the network protection has already been activated. + * @return The created notification. + */ + public static Notification createStatusNotification(VPNStates currentState, boolean protectionEnabled) { + // Start updating the data transmission stats, if needed. + if (dataUnitsSubscription == null) { + dataUnitsSubscription = VPNGeneralPersistentData.getDataUnitsObservable().subscribe(response -> { + dataUnits = response; + }); + } + + // The title is always "preparing", unless the state indicates the service is connected, + // disconnecting or restoring. For the state numeric values, check the emun documentation. + int title = R.string.vpn_service_state_preparing; + if (currentState == VPNStates.CONNECTED) { + title = VPNStates.getTitleForState(currentState); + } else { + if (currentState.val() >= VPNStates.DISCONNECTING.val()) { + title = R.string.vpn_service_state_finishing; + } else if (currentState.val() >= VPNStates.RESTORING_VPN.val() && currentState.val() < VPNStates.DISCONNECTING.val()) { + title = R.string.vpn_service_state_restoring; + } + } + + // Main text for the notification. + String text = App.getContext().getString(VPNStates.getDescriptionForState(currentState)); + // If connected, the connection stats are shown as the main text. + if (currentState == VPNStates.CONNECTED) { + text = "\u2191" + HelperFunctions.computeDataAmountString(Skywiremob.vpnBandwidthSent(), true, dataUnits != Globals.DataUnits.OnlyBytes); + text += " \u2193" + HelperFunctions.computeDataAmountString(Skywiremob.vpnBandwidthReceived(), true, dataUnits != Globals.DataUnits.OnlyBytes); + text += " \u2194" + HelperFunctions.getLatencyValue(Skywiremob.vpnLatency()); + } + + // The lines icon indicates that the service is disconnected and the network protection is + // not active. The filed icon indicates that the service is connected and working. The + // alert icon indicates that the network protection is active, but the VPN service is still + // not working. The error icon is used only if an error stopped the service. + int icon = R.drawable.ic_lines; + if (protectionEnabled) { + if (currentState == VPNStates.CONNECTED) { + icon = R.drawable.ic_filled; + } else { + icon = R.drawable.ic_alert; + } + } + if (currentState == VPNStates.ERROR || currentState == VPNStates.BLOCKING_ERROR) { + icon = R.drawable.ic_error; + } + + // Create the style for a multiline notification. It will be ignore if the OS does not + // support it. + NotificationCompat.BigTextStyle bigTextStyle = new NotificationCompat.BigTextStyle() + .bigText(text) + .setBigContentTitle(App.getContext().getString(title)); + + return new NotificationCompat.Builder(App.getContext(), NOTIFICATION_CHANNEL_ID) + .setSmallIcon(icon) + .setContentTitle(App.getContext().getString(title)) + .setContentText(text) + .setStyle(bigTextStyle) + .setContentIntent(HelperFunctions.getOpenAppPendingIntent()) + .setOnlyAlertOnce(true) + .setSound(null) + .build(); + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/helpers/UiMaterialIcons.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/helpers/UiMaterialIcons.java new file mode 100644 index 000000000..e1883ed10 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/helpers/UiMaterialIcons.java @@ -0,0 +1,6 @@ +package com.skywire.skycoin.vpn.helpers; + +public enum UiMaterialIcons { + MENU, + BACK, +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/network/ApiClient.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/network/ApiClient.java new file mode 100644 index 000000000..9b385f3b0 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/network/ApiClient.java @@ -0,0 +1,69 @@ +package com.skywire.skycoin.vpn.network; + +import com.skywire.skycoin.vpn.network.models.IpModel; +import com.skywire.skycoin.vpn.network.models.VpnServerModel; + +import java.util.List; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.schedulers.Schedulers; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory; +import retrofit2.converter.gson.GsonConverterFactory; +import retrofit2.converter.scalars.ScalarsConverterFactory; +import retrofit2.http.GET; +import retrofit2.http.Query; +import retrofit2.http.Url; + +public class ApiClient { + + private interface ApiInterface { + @GET("services") + Observable>> getVpnServers(@Query("type") String type); + + @GET + Observable> checkConnection(@Url String url); + + @GET + Observable> checkCurrentIp(@Url String url); + } + + private interface RawTextApiInterface { + @GET + Observable> checkIpCountry(@Url String url); + } + + public static final String BASE_URL = "https://service.discovery.skycoin.com/api/"; + + private static final Retrofit retrofit = new Retrofit.Builder() + .baseUrl(BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(RxJava3CallAdapterFactory.createWithScheduler(Schedulers.io())) + .build(); + + private static final Retrofit rawTextRetrofit = new Retrofit.Builder() + .baseUrl(BASE_URL) + .addConverterFactory(ScalarsConverterFactory.create()) + .addCallAdapterFactory(RxJava3CallAdapterFactory.createWithScheduler(Schedulers.io())) + .build(); + + private static final ApiInterface apiService = retrofit.create(ApiInterface.class); + private static final RawTextApiInterface rawTextApiService = rawTextRetrofit.create(RawTextApiInterface.class); + + public static Observable>> getVpnServers() { + return apiService.getVpnServers("vpn"); + } + + public static Observable> checkConnection(String url) { + return apiService.checkConnection(url); + } + + public static Observable> getCurrentIp() { + return apiService.checkCurrentIp("https://api.ipify.org/?format=json"); + } + + public static Observable> getIpCountry(String ip) { + return rawTextApiService.checkIpCountry("https://ip2c.org/" + ip); + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/network/models/GeoInfoModel.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/network/models/GeoInfoModel.java new file mode 100644 index 000000000..d8c067163 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/network/models/GeoInfoModel.java @@ -0,0 +1,8 @@ +package com.skywire.skycoin.vpn.network.models; + +public class GeoInfoModel { + public Double lat; + public Double lon; + public String country; + public String region; +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/network/models/IpModel.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/network/models/IpModel.java new file mode 100644 index 000000000..e797c1fdf --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/network/models/IpModel.java @@ -0,0 +1,5 @@ +package com.skywire.skycoin.vpn.network.models; + +public class IpModel { + public String ip; +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/network/models/VpnServerModel.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/network/models/VpnServerModel.java new file mode 100644 index 000000000..b54869a88 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/network/models/VpnServerModel.java @@ -0,0 +1,6 @@ +package com.skywire.skycoin.vpn.network.models; + +public class VpnServerModel { + public String addr; + public GeoInfoModel geo; +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/objects/LocalServerData.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/objects/LocalServerData.java new file mode 100644 index 000000000..518e1fc39 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/objects/LocalServerData.java @@ -0,0 +1,18 @@ +package com.skywire.skycoin.vpn.objects; + +import java.util.Date; + +public class LocalServerData { + public String countryCode; + public String name; + public String customName; + public String pk; + public Date lastUsed; + public boolean inHistory; + public ServerFlags flag; + public String location; + public String note; + public String personalNote; + public String password; + public boolean enteredManually; +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/objects/ManualVpnServerData.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/objects/ManualVpnServerData.java new file mode 100644 index 000000000..2255fbd22 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/objects/ManualVpnServerData.java @@ -0,0 +1,8 @@ +package com.skywire.skycoin.vpn.objects; + +public class ManualVpnServerData { + public String name; + public String password; + public String pk; + public String note; +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/objects/ServerFlags.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/objects/ServerFlags.java new file mode 100644 index 000000000..3a2ea5fc6 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/objects/ServerFlags.java @@ -0,0 +1,7 @@ +package com.skywire.skycoin.vpn.objects; + +public enum ServerFlags { + None, + Favorite, + Blocked +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/objects/ServerRatings.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/objects/ServerRatings.java new file mode 100644 index 000000000..8d178160a --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/objects/ServerRatings.java @@ -0,0 +1,38 @@ +package com.skywire.skycoin.vpn.objects; + +import com.skywire.skycoin.vpn.R; + +// TODO: for currently commented fields, must be deleted or reactivated depending on what happens to the fields. +//public enum ServerRatings { +// Gold, +// Silver, +// Bronze; +// +// /** +// * Allows to get the resource ID of the string corresponding to the rating. If no resource is +// * found for the rating, -1 is returned. +// */ +// public static int getTextForRating(ServerRatings rating) { +// if (rating == Gold) { +// return R.string.rating_gold; +// } else if (rating == Silver) { +// return R.string.rating_silver; +// } else if (rating == Bronze) { +// return R.string.rating_bronze; +// } +// +// return -1; +// } +// +// public static int getNumberForRating(ServerRatings rating) { +// if (rating == Gold) { +// return 2; +// } else if (rating == Silver) { +// return 1; +// } else if (rating == Bronze) { +// return 0; +// } +// +// return -1; +// } +//} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/vpn/SkywireVPNConnection.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/vpn/SkywireVPNConnection.java new file mode 100644 index 000000000..fca375bd8 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/vpn/SkywireVPNConnection.java @@ -0,0 +1,312 @@ +package com.skywire.skycoin.vpn.vpn; + +import com.skywire.skycoin.vpn.helpers.Globals; +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.App; +import com.skywire.skycoin.vpn.helpers.HelperFunctions; + +import java.io.Closeable; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.channels.DatagramChannel; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.ObservableEmitter; +import io.reactivex.rxjava3.core.ObservableOnSubscribe; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.schedulers.Schedulers; +import skywiremob.Skywiremob; + +/** + * Class in charge of finishing starting the visor and connect it with the VPN work interface, + * to make the VPN functional. + */ +public class SkywireVPNConnection implements Closeable { + /** + * Object for controlling the local visor. + */ + private final VisorRunnable visorRunnable; + /** + * Current VPN work interface. + */ + private VPNWorkInterface vpnInterface; + /** + * Tunnel for communicating with the local visor. + */ + private DatagramChannel tunnel = null; + + /** + * Allows to know if any of the procedures for sending and receiving data finished. + */ + private boolean managerFinished = false; + /** + * Error message returned during the last call to the function for making the VPN connection + * work, if any. + */ + private String lastError = null; + /** + * Last error returned by a procedure for sending or receiving data in another thread, if any. + */ + private Throwable operationError = null; + /** + * Observable used by this instance to make the VPN connection work. + */ + private Observable observable; + + private Disposable sendingProcedureSubscription; + private Disposable receivingProcedureSubscription; + + public SkywireVPNConnection( + VisorRunnable visorRunnable, + VPNWorkInterface vpnInterface + ) { + this.visorRunnable = visorRunnable; + this.vpnInterface = vpnInterface; + } + + /** + * Stops all operations and frees the resources used by this instance. + */ + @Override + public void close() { + closeConnection(); + } + + /** + * Creates an observable with the procedure for finishing the visor initialization and + * connecting the VPN interface with it, which makes the whole VPN protection start working. + * @return Observable which emits the current state, using the constants defined in VPNStates. + * The observable is not expected to complete, just emit and return errors. + */ + public Observable getObservable() { + // A new observable is created only if needed. + if (observable == null) { + observable = Observable.create((ObservableOnSubscribe) emitter -> { + try { + Skywiremob.printString("Starting VPN connection"); + + if (VPNGeneralPersistentData.getMustRestartVpn()) { + // The code will restart the connection in case of problem, but only if + // the connection was established during the last attempt. + while (true) { + // Stop if the emitter is no longer valid. + if (emitter.isDisposed()) { return; } + + lastError = null; + + // Break if the attempt was not able to finish the connection. + if (!run(emitter)) { + break; + } + + // Retry after a small delay. + emitter.onNext(VPNStates.RESTORING_VPN); + if (emitter.isDisposed()) { + return; + } + Thread.sleep(2000); + } + } else { + // Try to make the connection one time only. + run(emitter); + } + + // Finish with an error. + if (lastError == null) { + HelperFunctions.logError("VPN connection", "The connection has been closed unexpectedly."); + if (emitter.isDisposed()) { return; } + emitter.onError(new Exception(App.getContext().getString(R.string.vpn_connection_finished_error))); + } else { + HelperFunctions.logError("VPN connection", lastError); + if (emitter.isDisposed()) { return; } + emitter.onError(new Exception(lastError)); + } + } catch (Exception e) { + HelperFunctions.logError("The VPN connection failed, exiting", e); + if (!emitter.isDisposed()) { + emitter.onError(e); + } + } + + // This should never happen, as an error should have been reported before. + if (emitter.isDisposed()) { return; } + emitter.onComplete(); + }); + } + + return observable; + } + + /** + * Finish the visor initialization and connects the VPN interface with it, establishing the + * VPN connection. It is expected to run indefinitely and return only in case of error. + * @return True if the connections was established before the function finished. + */ + private boolean run(ObservableEmitter parentEmitter) { + boolean connected = false; + + managerFinished = false; + + // Reset the error vars, to indicate that no errors have occurred during this execution of + // the function. + lastError = null; + operationError = null; + + // TODO: delete if the code for protecting the sockets is removed. + // String protectErrorMsg = App.getContext().getString(R.string.vpn_socket_protection_error); + + try { + // Finish the visor initialization. + visorRunnable.runVpnClient(parentEmitter); + + // Create a DatagramChannel for connecting with the local visor. + if (parentEmitter.isDisposed()) { return connected; } + tunnel = DatagramChannel.open(); + + // TODO: this code is used for protecting the sockets (make them bypass vpn protection) + // needed for configuration, to avoid infinite loops. This is not currently needed + // because there is an exception that covers the entire application. The code remains + // here as a precaution and should be removed in the future. + /* + // Protect the tunnel before connecting to avoid loopback. + if (parentEmitter.isDisposed()) { return connected; } + if (!service.protect(tunnel.socket())) { + HelperFunctions.logError(getTag(), "Cannot protect the app-visor socket"); + throw new IllegalStateException(protectErrorMsg); + } + while(true) { + if (parentEmitter.isDisposed()) { return connected; } + + int fd = (int) Skywiremob.nextDmsgSocket(); + if (fd == 0) { break; } + + Skywiremob.printString("PRINTING FD " + fd); + if (!service.protect(fd)) { + HelperFunctions.logError(getTag(), "Cannot protect the socket for " + fd); + throw new IllegalStateException(protectErrorMsg); + } + } + */ + + // Connect to the local visor. + if (parentEmitter.isDisposed()) { return connected; } + tunnel.connect(new InetSocketAddress(Globals.LOCAL_VISOR_ADDRESS, Globals.LOCAL_VISOR_PORT)); + + // Inform the local socket address to Skywiremob. + // NOTE: this function should work in old Android versions, but there is a bug, at + // least in Android API 17, which makes the port to always be 0, that is why the app + // requires Android API 21+ to run. Maybe creating the socket by hand would allow to + // support older versions. + if (parentEmitter.isDisposed()) { return connected; } + Skywiremob.setMobileAppAddr(tunnel.socket().getLocalSocketAddress().toString()); + + // Make the data operations synchronous. + tunnel.configureBlocking(true); + // Configure the virtual network interface. This activates the VPN protection in the + // OS, if it is being done for the first time. + if (parentEmitter.isDisposed()) { return connected; } + vpnInterface.configure(VPNWorkInterface.Modes.WORKING); + // Inform the connection. + if (parentEmitter.isDisposed()) { return connected; } + connected = true; + parentEmitter.onNext(VPNStates.CONNECTED); + + Skywiremob.printString("The VPN connection is forwarding packets on Android"); + + // Create an observable for sending data in another thread. + sendingProcedureSubscription = VPNDataManager.createObservable(vpnInterface, tunnel, true) + .subscribeOn(Schedulers.newThread()).subscribe( + val -> {}, + err -> { + synchronized (this) { + // Save the error, to use it below. + if (operationError == null) { + operationError = err; + } + } + + stopWaiting(); + }, + () -> stopWaiting() + ); + // Create an observable for receiving data in another thread. + receivingProcedureSubscription = VPNDataManager.createObservable(vpnInterface, tunnel, false) + .subscribeOn(Schedulers.newThread()).subscribe( + val -> {}, + err -> { + synchronized (this) { + // Save the error, to use it below. + if (operationError == null) { + operationError = err; + } + } + + stopWaiting(); + }, + () -> stopWaiting() + ); + + synchronized (this) { + // Stop the thread until receiving a signal. If the observable is disposed while + // the thread is still waiting, an error will be thrown and it will be caught below. + if (!managerFinished) { + this.wait(); + } + + // If an error was saved while the thread was waiting, throw it. + if (operationError != null) { + throw operationError; + } + } + } catch (Throwable e) { + // Report the error. + if (!parentEmitter.isDisposed()) { + HelperFunctions.logError("VPN connector work procedure", e); + lastError = e.getLocalizedMessage(); + } + } finally { + // CLose the connection. + closeConnection(); + } + + return connected; + } + + /** + * Reactivates the thread after being stopped in the run() function. + */ + private void stopWaiting() { + synchronized (this) { + managerFinished = true; + + try { + this.notify(); + } catch (Exception e) { } + } + } + + /** + * Closes any open connection, stops the VPN client and stops the the pending threads. + */ + private void closeConnection() { + if (sendingProcedureSubscription != null) { + sendingProcedureSubscription.dispose(); + } + if (receivingProcedureSubscription != null) { + receivingProcedureSubscription.dispose(); + } + + visorRunnable.stopVpnConnection(); + + if (tunnel != null) { + try { + tunnel.close(); + tunnel = null; + } catch (IOException e) { + HelperFunctions.logError("Unable to close tunnel used by the VPN connection", e); + } + } + + stopWaiting(); + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/vpn/SkywireVPNService.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/vpn/SkywireVPNService.java new file mode 100644 index 000000000..ad5ebe9b9 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/vpn/SkywireVPNService.java @@ -0,0 +1,508 @@ +package com.skywire.skycoin.vpn.vpn; + +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import android.net.VpnService; +import android.os.Bundle; +import android.os.Message; +import android.os.Messenger; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.App; +import com.skywire.skycoin.vpn.helpers.HelperFunctions; +import com.skywire.skycoin.vpn.helpers.Notifications; +import com.skywire.skycoin.vpn.objects.ServerFlags; + +import java.util.concurrent.TimeUnit; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.schedulers.Schedulers; +import skywiremob.Skywiremob; + +/** + * Service in charge of making the VPN protection work, even if the UI is closed. + */ +public class SkywireVPNService extends VpnService { + /** + * Action that must be sent to the service for starting the VPN connection. If + * the connection has already been started, it continues running normally. + */ + public static final String ACTION_CONNECT = "com.skywire.android.vpn.START"; + /** + * Action that must be sent to the service for stopping the VPN connection. The procedure may + * take some time to complete, so the state events must be monitored. + */ + public static final String ACTION_DISCONNECT = "com.skywire.android.vpn.STOP"; + + /** + * Param returned by the service as part of the state updates, for including the error + * message, if the state includes one. + */ + public static final String ERROR_MSG_PARAM = "ErrorMsg"; + /** + * Param returned by the service as part of the state updates, for informing if the service is + * running because the OS requested it (true) or was started by the app itself (false). + */ + public static final String STARTED_BY_THE_SYSTEM_PARAM = "StartedByTheSystem"; + /** + * Param returned by the service as part of the state updates, for informing if it has received + * a request for completely stopping the service. The request may have not been made by + * the user. + */ + public static final String STOP_REQUESTED_PARAM = "StopRequested"; + + /** + * ID of the last instance of the service. This is needed because a new instance may be + * created by the OS while the previous one is still being destroyed and in those cases it is + * necessary to stop making some operations in the old instance. + */ + public static int lastInstanceID = 0; + /** + * ID of this object instance. If it is not equal to lastInstanceID, this is not the + * latest instance. + */ + public int instanceID = 0; + + /** + * Object for showing notifications. + */ + private final NotificationManager notificationManager = (NotificationManager) App.getContext().getSystemService(Context.NOTIFICATION_SERVICE); + + /** + * Instance for communicating with the VPN coordinator class. + */ + private Messenger messenger; + + /** + * Object in charge of performing the steps needed for making the VPN protection work. + */ + private VPNRunnable vpnRunnable; + /** + * Current VPN work interface. + */ + private VPNWorkInterface vpnInterface; + + /** + * Current state of the VPN protection. + */ + private VPNStates currentState = VPNStates.STARTING; + + /** + * If the service is running because the OS requested it (true) or was started by the app + * itself (false). + */ + private boolean startedByTheSystem = false; + /** + * If true, a condition that makes it not possible to start the service was detected, so + * the option for retrying the connection must be ignored. + */ + private boolean impossibleToStart = false; + /** + * If there was a request for completely stopping the service. + */ + private boolean stopRequested = false; + /** + * If the service has already been destroyed. The code may still be running cleaning procedures. + */ + private boolean serviceDestroyed = false; + + /** + * Msg of the last error detected by this instance. + */ + private String lastErrorMsg = ""; + + private Disposable updateNotificationSubscription; + private Disposable restartingSubscription; + private Disposable vpnRunnableSubscription; + + /** + * Informs the current state to the VPN coordinator, updates the state notification and shows + * toast notifications, if needed. It also updates the current state var. + */ + private void informNewState(VPNStates newState) { + // Cancel the operation if there is a newer instance of the service. + if (lastInstanceID != instanceID) { + return; + } + + // Create a new message for informing the VPN coordinator about the new state. + Message msg = Message.obtain(); + msg.what = newState.val(); + + // Add the additional data to the message. + Bundle dataBundle = new Bundle(); + dataBundle.putBoolean(STARTED_BY_THE_SYSTEM_PARAM, startedByTheSystem); + dataBundle.putBoolean(STOP_REQUESTED_PARAM, stopRequested); + + // Get the last error from vpnRunnable.getLastErrorMsg(). The lastErrorMsg must be used + // to avoid errors because vpnRunnable may be null. + lastErrorMsg = vpnRunnable != null ? vpnRunnable.getLastErrorMsg() : lastErrorMsg; + dataBundle.putString(ERROR_MSG_PARAM, lastErrorMsg); + + msg.setData(dataBundle); + + // Show toast notifications for certain states if the UI is not being shown. + if (!App.displayingUI() && currentState != newState) { + // Only if the service has not been destroyed. + if (!serviceDestroyed && (newState == VPNStates.CONNECTED || + newState == VPNStates.RESTORING_VPN || + newState == VPNStates.RESTORING_SERVICE || + newState == VPNStates.ERROR || + newState == VPNStates.BLOCKING_ERROR)) + { + HelperFunctions.showToast(getString(VPNStates.getDescriptionForState(newState)), false); + } + + // Even if the service has been destroyed. + if (newState == VPNStates.DISCONNECTED || newState == VPNStates.DISCONNECTING || newState == VPNStates.OFF) { + HelperFunctions.showToast(getString(VPNStates.getDescriptionForState(newState)), false); + } + } + + currentState = newState; + + // Send the message to the VPN coordinator. + try { + messenger.send(msg); + } catch (Exception e) { } + + // Update the notification. + updateForegroundNotification(); + + // Procedure for periodically updating the notification with the connection stats, if the + // VPN protection is active. + if (updateNotificationSubscription != null) { + updateNotificationSubscription.dispose(); + } + if (newState == VPNStates.CONNECTED) { + updateNotificationSubscription = Observable.interval(2000, TimeUnit.MILLISECONDS) + .subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(val -> updateForegroundNotification()); + } + } + + /** + * Function that must be called when there are changes in the state of the VPN protection. It + * processes the new state, makes some preparations and informs it. + */ + private void updateState(VPNStates newState) { + // State that will be reported at the end of the function. It may be modified. + VPNStates processedState = newState; + + // If the current state is for indicating an error and the new state is for indicating + // that the VPN protection is being disconnected, the current state is maintained, to + // avoid replacing the error indications, which is more useful than a generic indication + // about the service being stopped. This also prevents the code from "forgetting" that + // there was an error, which may be important later. + if (processedState.val() >= 200 && processedState.val() < 300 && currentState.val() >= 400 && currentState.val() <= 500) { + processedState = currentState; + } + + boolean failedBecausePassword = false; + // If the state indicates that vpnRunnable finished, remove the instance. + if (processedState.val() >= 300 && processedState.val() < 400) { + // Check if the process finished due to an error cause by a wrong password. This data is + // used if the protection has to be restarted. + if (vpnRunnable != null && vpnRunnable.getIfPasswordFailed()) { + failedBecausePassword = true; + } + vpnRunnable = null; + if (vpnRunnableSubscription != null) { + vpnRunnableSubscription.dispose(); + } + } + + // Only needed if the service is not forced to terminate. + if (!stopRequested && !serviceDestroyed) { + // If the new state is for informing about an error. + if (processedState.val() >= 400 && processedState.val() < 500) { + if (VPNGeneralPersistentData.getMustRestartVpn() && !impossibleToStart) { + // If the option for restarting the protection automatically is active, update + // the state. + processedState = VPNStates.RESTORING_SERVICE; + } else if (processedState == VPNStates.ERROR) { + // If the error was not a blocking one, which would mean that the network must + // remain blocked, indicate that the service must be closed after closing + // the VPN. + stopRequested = true; + } + } + + // If the service is being restored, hide the states about the connection being + // closed and restored. + if (currentState == VPNStates.RESTORING_SERVICE) { + // Restart the whole VPN connection after a small delay when receiving the state + // indicating that vpnRunnable finished. If the error was because the password was + // wrong, the delay is much longer. + if (processedState.val() >= 300 && processedState.val() < 400) { + int delay = failedBecausePassword ? 60000 : 1; + restartingSubscription = Observable.just(0).delay(delay, TimeUnit.MILLISECONDS) + .subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(val -> runVpn()); + } + + if (processedState.val() >= 150 && processedState.val() < 400) { + processedState = VPNStates.RESTORING_SERVICE; + } + } else { + // If the service is not being restored, close the whole service when receiving + // the state indicating that vpnRunnable finished. + if (processedState.val() >= 300 && processedState.val() < 400) { + processedState = currentState; + finishIfAppropriate(); + } + } + } else { + // Close the whole service when receiving the state indicating that + // vpnRunnable finished. + if (processedState.val() >= 300 && processedState.val() < 400) { + processedState = currentState; + finishIfAppropriate(); + } + } + + // Inform the new state to the VPN coordinator and update the notifications. + informNewState(processedState); + } + + /** + * Function called by the OS just after receiving an instruction for starting the service. + */ + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + // Update the ID of this instance, to make sure no old instance is considered newer than + // this one. + lastInstanceID += 1; + instanceID = lastInstanceID; + + if (intent != null && ACTION_DISCONNECT.equals(intent.getAction())) { + // If this function was called to stop the VPN protection. + + stopRequested = true; + + // Stop the connection. If it was already stopped, finish the service directly. + if (vpnRunnable != null) { + vpnRunnable.disconnect(); + } else { + finishIfAppropriate(); + } + + // Needed for informing the new value of the stopRequested var. + updateState(currentState); + } else { + // If the function was not called for stopping the VPN protection, it is considered + // that it was called for starting it. In this case, the instruction for starting the + // service may have been made by the OS or the app itself. if the ACTION_CONNECT action + // is not detected, it is considered that the request was made by the OS. + + // Get the object for communicating with the VPN coordinator. + if (messenger == null) { + messenger = VPNCoordinator.getInstance().getCommunicationMessenger(); + } + + if (vpnInterface == null) { + // Become a foreground service. Background services can be VPN services too, but + // they can be killed by background check before getting a chance to + // receive onRevoke(). + makeForeground(); + + vpnInterface = new VPNWorkInterface(this); + } + + // If the option for blocking the network while configuring the service is active or + // the request was made by the OS, the VPN work interface is configured, to block all + // network connections. The action is always made when the service is started by the OS + // because the OS will only stop the service after the user request it if the interface + // is configured (appears like a bug in the OS). + if (!vpnInterface.alreadyConfigured() && (VPNGeneralPersistentData.getProtectBeforeConnected() || intent == null || !ACTION_CONNECT.equals(intent.getAction()))) { + try { + vpnInterface.configure(VPNWorkInterface.Modes.BLOCKING); + } catch (Exception e) { + // Report the error and finish the service. + HelperFunctions.logError("Configuring VPN work interface before connecting", e); + lastErrorMsg = getString(R.string.vpn_service_network_protection_error); + updateState(VPNStates.ERROR); + finishIfAppropriate(); + + return START_NOT_STICKY; + } + + if (intent == null || !ACTION_CONNECT.equals(intent.getAction())) { + HelperFunctions.showToast(getString(R.string.vpn_service_network_unavailable_warning), false); + } + } + + // Update if the service was started by the OS and notify it in a state event. Note + // that this code updates the previous value if the service was originally started by + // the app, this is intended. + if (intent == null || !ACTION_CONNECT.equals(intent.getAction())) { + startedByTheSystem = true; + } + updateState(currentState); + + // Check if no server has been selected and if the selected server has been blocked. + String errorMsg = null; + if ( + VPNServersPersistentData.getInstance().getCurrentServer() == null || + VPNServersPersistentData.getInstance().getCurrentServer().pk == null || + VPNServersPersistentData.getInstance().getCurrentServer().pk.trim().equals("") + ) { + errorMsg = App.getContext().getText(R.string.skywiremob_error_no_server).toString(); + } else if (VPNServersPersistentData.getInstance().getCurrentServer().flag == ServerFlags.Blocked) { + errorMsg = App.getContext().getText(R.string.skywiremob_error_server_blocked).toString(); + } + + // If any of the previous conditions was found, put the service in error state. + if (errorMsg != null) { + HelperFunctions.logError("Starting VPN service", errorMsg); + lastErrorMsg = errorMsg; + impossibleToStart = true; + updateState(VPNStates.ERROR); + } else { + // Start the VPN protection. + runVpn(); + } + } + + return START_NOT_STICKY; + } + + /** + * Function called by the OS when the service is destroyed. + */ + @Override + public void onDestroy() { + Skywiremob.printString("VPN service destroyed."); + serviceDestroyed = true; + + // Stop the connection. If it was already stopped, finish the service directly. + if (vpnRunnable != null) { + vpnRunnable.disconnect(); + } else { + finishIfAppropriate(); + } + } + + /** + * Function called by the OS when the user revokes the permission for the VPN. + */ + @Override + public void onRevoke() { + super.onRevoke(); + Skywiremob.printString("onRevoke called"); + // Destroy the service. + this.stopSelf(); + } + + /** + * Starts the VPN protection, if it is not already active or starting. + */ + private void runVpn() { + if (vpnRunnable == null) { + vpnRunnable = new VPNRunnable(vpnInterface); + } + + if (vpnRunnableSubscription != null) { + vpnRunnableSubscription.dispose(); + } + + // Initialize the VPN. Also, get and process the state updates. + vpnRunnableSubscription = vpnRunnable.start().subscribe(state -> updateState(state)); + } + + /** + * Cleans the resources used by the service and stops it, but only if vpnRunnable + * already finished. + */ + private void finishIfAppropriate() { + if (vpnRunnable == null) { + if (vpnInterface == null || + !vpnInterface.alreadyConfigured() || + stopRequested || + serviceDestroyed || + currentState.val() < 400 || + currentState.val() >= 500 || + !VPNGeneralPersistentData.getKillSwitchActivated() + ) { + // Steps that must be performed only if there is no a newer instance of the service. + if (lastInstanceID == instanceID) { + // Clean the VPN interface (which stops blocking the network connections). + if (vpnInterface != null) { + vpnInterface.close(); + + // Create another interface and close it immediately to avoid a bug in + // older Android versions when the app is added to the ignore list. + vpnInterface = new VPNWorkInterface(this); + try { + vpnInterface.configure(VPNWorkInterface.Modes.DELETING); + } catch (Exception e) { } + vpnInterface.close(); + } + + // Remove the state notification. + notificationManager.cancel(Notifications.SERVICE_STATUS_NOTIFICATION_ID); + + // Report the new state after a delay, to avoid interferences with any new + // state reported by the code which called this function. + Observable.just(0).delay(100, TimeUnit.MILLISECONDS) + .subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(val -> updateState(VPNStates.OFF)); + + // If there was an error in the last execution, the UI is not being displayed + // and the kill switch is not active, show a notification informing that + // the VPN protection was terminated due to an error. + if (!App.displayingUI() && !VPNGeneralPersistentData.getKillSwitchActivated() && VPNGeneralPersistentData.getLastError(null) != null) { + Notifications.showAlertNotification( + Notifications.ERROR_NOTIFICATION_ID, + getString(R.string.general_app_name), + getString(R.string.general_connection_error), + HelperFunctions.getOpenAppPendingIntent() + ); + } + } + + // Remove the objects and close the subscriptions. + vpnInterface = null; + vpnRunnable = null; + if (vpnRunnableSubscription != null) { + vpnRunnableSubscription.dispose(); + } + if (restartingSubscription != null) { + restartingSubscription.dispose(); + } + + // Terminate the service. + stopForeground(true); + stopSelf(); + } + } + } + + /** + * Updates the state notification shown while the service is running in the foreground. + */ + private void updateForegroundNotification() { + if (!serviceDestroyed) { + notificationManager.notify( + Notifications.SERVICE_STATUS_NOTIFICATION_ID, + Notifications.createStatusNotification(currentState, vpnInterface != null && vpnInterface.alreadyConfigured()) + ); + } + } + + /** + * Converts the service into a foreground service, to prevent it to be destroyed by the OS. + */ + private void makeForeground() { + startForeground( + Notifications.SERVICE_STATUS_NOTIFICATION_ID, + Notifications.createStatusNotification(currentState, vpnInterface != null && vpnInterface.alreadyConfigured()) + ); + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/vpn/VPNCoordinator.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/vpn/VPNCoordinator.java new file mode 100644 index 000000000..5689830d4 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/vpn/VPNCoordinator.java @@ -0,0 +1,315 @@ +package com.skywire.skycoin.vpn.vpn; + +import android.app.Activity; +import android.app.ActivityManager; +import android.content.Context; +import android.content.Intent; +import android.net.VpnService; +import android.os.Build; +import android.os.Handler; +import android.os.Message; +import android.os.Messenger; + +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.App; +import com.skywire.skycoin.vpn.helpers.HelperFunctions; +import com.skywire.skycoin.vpn.helpers.Notifications; +import com.skywire.skycoin.vpn.objects.LocalServerData; + +import java.util.ArrayList; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.BehaviorSubject; +import skywiremob.Skywiremob; + +import static android.app.Activity.RESULT_OK; + +/** + * Class for communication between the app UI and the VPN service. It is accessed via a singleton. + */ +public class VPNCoordinator implements Handler.Callback { + public static class ConnectionStats { + public Date lastConnectionDate = null; + public long currentDownloadSpeed = 0; + public long currentUploadSpeed = 0; + public long currentLatency = 0; + public long totalDownloadedData = 0; + public long totalUploadedData = 0; + public ArrayList downloadSpeedHistory = new ArrayList<>(); + public ArrayList uploadSpeedHistory = new ArrayList<>(); + public ArrayList latencyHistory = new ArrayList<>(); + + public ConnectionStats() { + for (int i = 0; i < 10; i++) { + downloadSpeedHistory.add(0L); + uploadSpeedHistory.add(0L); + latencyHistory.add(0L); + } + } + } + + /** + * Value the onActivityResult function will get after asking the user for permission. + */ + public static final int VPN_PREPARATION_REQUEST_CODE = 10100; + + /** + * Singleton instance. + */ + private static final VPNCoordinator instance = new VPNCoordinator(); + /** + * Gets the singleton for using the class. + */ + public static VPNCoordinator getInstance() { return instance; } + + private Disposable updateStatsSubscription; + + private ConnectionStats connectionStats = new ConnectionStats(); + + /** + * App context. + */ + private final Context ctx = App.getContext(); + + /** + * Handler used for receiving messages from the VPN service. + */ + private final Handler serviceCommunicationHandler; + /** + * Subject for sending events via RxJava, indicating the current state of the VPN service. + */ + private final BehaviorSubject eventsSubject = BehaviorSubject.create(); + + private final BehaviorSubject connectionStatsSubject = BehaviorSubject.create(); + + private VPNCoordinator() { + serviceCommunicationHandler = new Handler(this); + + // Add a default current state. + eventsSubject.onNext(new VPNStates.StateInfo(VPNStates.OFF, false, false)); + } + + public Observable getConnectionStats() { + return connectionStatsSubject.hide(); + } + + /** + * Handles the messages received from the VPN service. + */ + @Override + public boolean handleMessage(Message msg) { + // Save the error as the one which made the last execution of the VPN service fail. + // Must be done before sending the event. + String errorMsg = msg.getData().getString(SkywireVPNService.ERROR_MSG_PARAM); + if (errorMsg != null && !errorMsg.equals("") && !errorMsg.equals(VPNGeneralPersistentData.getLastError(null))) { + VPNGeneralPersistentData.setLastError(errorMsg); + } + + if (updateStatsSubscription == null) { + continuallyUpdateStats(); + } + + if (VPNStates.valueOf(msg.what) == VPNStates.CONNECTED) { + // Erase the error which made not possible to connect the last time. + VPNGeneralPersistentData.removeLastError(); + + if (connectionStats.lastConnectionDate == null) { + connectionStats.lastConnectionDate = new Date(); + } + } else { + if (VPNStates.valueOf(msg.what) == VPNStates.DISCONNECTED || VPNStates.valueOf(msg.what) == VPNStates.OFF) { + if (updateStatsSubscription != null) { + updateStatsSubscription.dispose(); + updateStatsSubscription = null; + } + + connectionStats = new ConnectionStats(); + connectionStatsSubject.onNext(connectionStats); + } else { + connectionStats.lastConnectionDate = null; + } + } + + // Create the state object with the params returned by the VPN service. + VPNStates.StateInfo state = new VPNStates.StateInfo( + VPNStates.valueOf(msg.what), + msg.getData().getBoolean(SkywireVPNService.STARTED_BY_THE_SYSTEM_PARAM), + msg.getData().getBoolean(SkywireVPNService.STOP_REQUESTED_PARAM) + ); + + // Inform the new state. + eventsSubject.onNext(state); + + return true; + } + + private void continuallyUpdateStats() { + if (updateStatsSubscription != null) { + updateStatsSubscription.dispose(); + } + + sendStats(); + + updateStatsSubscription = Observable.interval(1000L, TimeUnit.MILLISECONDS) + .subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(val -> { + sendStats(); + }); + } + + private void sendStats() { + connectionStats.currentDownloadSpeed = Skywiremob.vpnBandwidthReceived(); + connectionStats.downloadSpeedHistory.remove(0); + connectionStats.downloadSpeedHistory.add(connectionStats.currentDownloadSpeed); + + connectionStats.currentUploadSpeed = Skywiremob.vpnBandwidthSent(); + connectionStats.uploadSpeedHistory.remove(0); + connectionStats.uploadSpeedHistory.add(connectionStats.currentUploadSpeed); + + connectionStats.currentLatency = Skywiremob.vpnLatency(); + connectionStats.latencyHistory.remove(0); + connectionStats.latencyHistory.add(connectionStats.currentLatency); + + connectionStatsSubject.onNext(connectionStats); + } + + /** + * Allows to know if the VPN service is currently running. + */ + public boolean isServiceRunning() { + ActivityManager manager = (ActivityManager) App.getContext().getSystemService(Context.ACTIVITY_SERVICE); + for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) { + // Check if any of the running services is the VPN service. + if (SkywireVPNService.class.getName().equals(service.service.getClassName())) { + return true; + } + } + return false; + } + + /** + * Returns an observable that emits every time the state of the VPN service changes. The + * observable does not emit errors and never completes. + */ + public Observable getEventsObservable() { + return eventsSubject.hide(); + } + + /** + * Makes the preparations and starts the VPN service. If it is already running, nothing happens. + * @param requestingActivity Activity requesting the service to be started. Please note + * that the onActivityResult function of that activity may be called with the value of + * VPN_PREPARATION_REQUEST_CODE as the first param. In that case the activity must call the + * onActivityResult function of this instance with all the params, to be able to process + * permission requests + * @param server Data about the remote visor. + */ + public void startVPN(Activity requestingActivity, LocalServerData server) { + if (!isServiceRunning()) { + // Save the remote visor and password. + VPNServersPersistentData.getInstance().modifyCurrentServer(server); + VPNServersPersistentData.getInstance().updateHistory(); + + // As the service will be started again, erase the error which made it fail the last + // time it ran, to indicate that no error has stopped the current instance. + VPNGeneralPersistentData.removeLastError(); + + eventsSubject.onNext(new VPNStates.StateInfo(VPNStates.STARTING, false, false)); + + // Get the permission request intent from the OS. + Intent intent = VpnService.prepare(requestingActivity); + if (intent != null) { + // Ask for permission before continuing. + requestingActivity.startActivityForResult(intent, VPN_PREPARATION_REQUEST_CODE); + } else { + starVpnServiceIfNeeded(); + } + } + } + + /** + * Function for starting the VPN service after boot. If the service is already running, + * nothing happens. + */ + public void activateAutostart() { + if (!isServiceRunning()) { + // Check if permission is needed. If it is, fail. + Intent intent = VpnService.prepare(ctx); + if (intent != null) { + HelperFunctions.showToast(ctx.getString(R.string.general_autostart_failed_error), false); + + String errorMsg = ctx.getString(R.string.general_no_permissions_error); + VPNGeneralPersistentData.setLastError(errorMsg); + + Notifications.showAlertNotification( + Notifications.AUTOSTART_ALERT_NOTIFICATION_ID, + ctx.getString(R.string.general_app_name), + errorMsg, + HelperFunctions.getOpenAppPendingIntent() + ); + + return; + } + + // As the service will be started again, erase the error which made it fail the last + // time it ran, to indicate that no error has stopped the current instance. + VPNGeneralPersistentData.removeLastError(); + + starVpnServiceIfNeeded(); + } + } + + /** + * Asks the VPN service to stop. It will not be stopped immediately, the state change events + * must be checked for knowing when it is really stopped. + */ + public void stopVPN() { + ctx.startService(getServiceIntent().setAction(SkywireVPNService.ACTION_DISCONNECT)); + } + + /** + * Must be called by the activity used for calling startVPN, if the same function is called + * in the activity and the value of VPN_PREPARATION_REQUEST_CODE was received as request. + * The same params received in the activity must be provided. + */ + public void onActivityResult(int request, int result, Intent data) { + if (request == VPN_PREPARATION_REQUEST_CODE) { + if (result == RESULT_OK) { + starVpnServiceIfNeeded(); + } else { + eventsSubject.onNext(new VPNStates.StateInfo(VPNStates.OFF, false, true)); + } + } + } + + /** + * Starts the VPN service if it is not already running. + */ + private void starVpnServiceIfNeeded() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + ctx.startForegroundService(getServiceIntent().setAction(SkywireVPNService.ACTION_CONNECT)); + } else { + ctx.startService(getServiceIntent().setAction(SkywireVPNService.ACTION_CONNECT)); + } + } + + /** + * Gets the VPN service intent, without action. + */ + private Intent getServiceIntent() { + return new Intent(ctx, SkywireVPNService.class); + } + + /** + * Gets a Messenger object for communicating with this instance. + */ + public Messenger getCommunicationMessenger() { + return new Messenger(serviceCommunicationHandler); + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/vpn/VPNDataManager.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/vpn/VPNDataManager.java new file mode 100644 index 000000000..9ebee7bff --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/vpn/VPNDataManager.java @@ -0,0 +1,85 @@ +package com.skywire.skycoin.vpn.vpn; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InterruptedIOException; +import java.nio.ByteBuffer; +import java.nio.channels.DatagramChannel; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.ObservableOnSubscribe; + +/** + * Helper class for creating an observable for sending or getting data to or from the visor. + */ +public class VPNDataManager { + /** + * Creates an observable for sending or getting data to or from the visor. + * @param vpnInterface Interface currently used for the VPN connection. + * @param tunnel Socket for communicating with the visor. + * @param forSending True if the observable will be used for sending the data from the OS to the + * visor, false if it is for sending the data from the visor to the OS. + */ + static public Observable createObservable(VPNWorkInterface vpnInterface, DatagramChannel tunnel, boolean forSending) { + return Observable.create((ObservableOnSubscribe) emitter -> { + // Streams for receiving and sending packages. + final FileInputStream in; + final FileOutputStream out; + // Only the stream needed is initialized. + if (forSending) { + in = vpnInterface.getInputStream(); + out = null; + } else { + in = null; + out = vpnInterface.getOutputStream(); + } + + ByteBuffer packet = ByteBuffer.allocate(Short.MAX_VALUE); + + // Get or send data while the emitter is still valid. + while(!emitter.isDisposed()) { + try { + if (forSending) { + // Read the outgoing packet from the input stream. The operation must block + // blocks the thread. + int length = in.read(packet.array()); + if (length > 0) { + // Write the outgoing packet to the tunnel. + packet.limit(length); + tunnel.write(packet); + packet.clear(); + } + } + + if (!forSending) { + // Read the incoming packet from the visor socket. The operation must block + // blocks the thread. + int length = tunnel.read(packet); + if (length > 0) { + // Ignore control messages, which start with zero. + if (packet.get(0) != 0) { + // Write the incoming packet to the output stream. + out.write(packet.array(), 0, length); + } + packet.clear(); + } + } + } catch (InterruptedIOException e) { + // This error is thrown if there is a timeout while waiting data from the socket. + // It is ignored so that the loop repeats itself to wait for data again. + } catch (Exception e) { + // Emit the error only if the emitter is still valid. + if (!emitter.isDisposed()) { + emitter.onError(e); + return; + } + + break; + } + } + + // Indicate the observable finished. + emitter.onComplete(); + }); + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/vpn/VPNGeneralPersistentData.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/vpn/VPNGeneralPersistentData.java new file mode 100644 index 000000000..e15b58c19 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/vpn/VPNGeneralPersistentData.java @@ -0,0 +1,238 @@ +package com.skywire.skycoin.vpn.vpn; + +import android.content.SharedPreferences; + +import androidx.preference.PreferenceManager; + +import com.google.gson.Gson; +import com.skywire.skycoin.vpn.App; +import com.skywire.skycoin.vpn.helpers.Globals; + +import java.util.HashSet; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.subjects.BehaviorSubject; + +/** + * Helper class for saving and getting general data related to the VPN to and from the + * persistent storage. + */ +public class VPNGeneralPersistentData { + // Keys for persistent storage. + private static final String LAST_ERROR = "lastError"; + private static final String DATA_UNITS = "dataUnits"; + private static final String CUSTOM_DNS = "customDns"; + private static final String APPS_SELECTION_MODE = "appsMode"; + private static final String APPS_LIST = "appsList"; + private static final String SHOW_IP = "showIp"; + private static final String KILL_SWITCH = "killSwitch"; + private static final String RESTART_VPN = "restartVpn"; + private static final String START_ON_BOOT = "startOnBoot"; + private static final String PROTECT_BEFORE_CONNECTED = "protectBeforeConnected"; + + private static final SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(App.getContext()); + + private static BehaviorSubject dataUnitsSubject; + + ///////////////////////////////////////////////////////////// + // Setters. + ///////////////////////////////////////////////////////////// + + /** + * Saves the message of the error which caused the VPN service to fail the last time it + * ran, if any. + */ + public static void setLastError(String val) { + settings.edit().putString(LAST_ERROR, val).apply(); + } + + /** + * Saves the data units that must be shown in the UI. + */ + public static void setDataUnits(Globals.DataUnits val) { + Gson gson = new Gson(); + String valString = gson.toJson(val); + settings.edit().putString(DATA_UNITS, valString).apply(); + + // Inform the change. + if (dataUnitsSubject != null) { + dataUnitsSubject.onNext(val); + } + } + + /** + * Saves the IP of the custom DNS server. + */ + public static void setCustomDns(String val) { + settings.edit().putString(CUSTOM_DNS, val).apply(); + } + + /** + * Saves the mode the VPN service must use to protect or ignore the apps selected by the user. + */ + public static void setAppsSelectionMode(Globals.AppFilteringModes val) { + settings.edit().putString(APPS_SELECTION_MODE, val.toString()).apply(); + } + + /** + * Saves the list with the package names of all apps selected by the user in the app list. + */ + public static void setAppList(HashSet val) { + settings.edit().putStringSet(APPS_LIST, val).apply(); + } + + /** + * Sets if the functionality for showing the IP must be active. + */ + public static void setShowIpActivated(boolean val) { + settings.edit().putBoolean(SHOW_IP, val).apply(); + } + + /** + * Sets if the kill switch functionality must be active. + */ + public static void setKillSwitchActivated(boolean val) { + settings.edit().putBoolean(KILL_SWITCH, val).apply(); + } + + /** + * Sets if the VPN connection must be automatically restarted if there is an error. + */ + public static void setMustRestartVpn(boolean val) { + settings.edit().putBoolean(RESTART_VPN, val).apply(); + } + + /** + * Sets if the VPN protection must be activated as soon as possible after booting the OS. + */ + public static void setStartOnBoot(boolean val) { + settings.edit().putBoolean(START_ON_BOOT, val).apply(); + } + + /** + * Sets if the network protection must be activated just after starting the VPN service, which + * would disable the internet connectivity for the rest of the apps while configuring the visor. + */ + public static void setProtectBeforeConnected(boolean val) { + settings.edit().putBoolean(PROTECT_BEFORE_CONNECTED, val).apply(); + } + + ///////////////////////////////////////////////////////////// + // Getters. + ///////////////////////////////////////////////////////////// + + /** + * Gets the message of the error which caused the VPN service to fail the last time it + * ran, if any. + * @param defaultValue Value to return if no saved data is found. + */ + public static String getLastError(String defaultValue) { + return settings.getString(LAST_ERROR, defaultValue); + } + + /** + * Returns the data units that must be shown in the UI. If the user has not changed + * the setting, it returns DataUnits.BitsSpeedAndBytesVolume by default. + */ + public static Globals.DataUnits getDataUnits() { + Gson gson = new Gson(); + String savedVal = settings.getString(DATA_UNITS, null); + if (savedVal != null) { + return gson.fromJson(savedVal, Globals.DataUnits.class); + } + + return Globals.DataUnits.BitsSpeedAndBytesVolume; + } + + /** + * Emits every time the data units that must be shown in the UI are changed. It emits the most + * recent value immediately after subscription. + */ + public static Observable getDataUnitsObservable() { + if (dataUnitsSubject == null) { + dataUnitsSubject = BehaviorSubject.create(); + dataUnitsSubject.onNext(getDataUnits()); + } + + return dataUnitsSubject.hide(); + } + + /** + * Gets the IP of the custom DNS server. + */ + public static String getCustomDns() { + return settings.getString(CUSTOM_DNS, null); + } + + /** + * Gets the mode the VPN service must use to protect or ignore the apps selected by the user. + */ + public static Globals.AppFilteringModes getAppsSelectionMode() { + String savedValue = settings.getString(APPS_SELECTION_MODE, null); + + if (savedValue == null || savedValue.equals(Globals.AppFilteringModes.PROTECT_ALL.toString())) { + return Globals.AppFilteringModes.PROTECT_ALL; + } else if (savedValue.equals(Globals.AppFilteringModes.PROTECT_SELECTED.toString())) { + return Globals.AppFilteringModes.PROTECT_SELECTED; + } else if (savedValue.equals(Globals.AppFilteringModes.IGNORE_SELECTED.toString())) { + return Globals.AppFilteringModes.IGNORE_SELECTED; + } + + return Globals.AppFilteringModes.PROTECT_ALL; + } + + /** + * Gets the list with the package names of all apps selected by the user in the app list. + * @param defaultValue Value to return if no saved data is found. + */ + public static HashSet getAppList(HashSet defaultValue) { + return new HashSet<>(settings.getStringSet(APPS_LIST, defaultValue)); + } + + /** + * Gets if the functionality for showing the IP must be active. + */ + public static boolean getShowIpActivated() { + return settings.getBoolean(SHOW_IP, true); + } + + /** + * Gets if the kill switch functionality must be active. + */ + public static boolean getKillSwitchActivated() { + return settings.getBoolean(KILL_SWITCH, true); + } + + /** + * Gets if the VPN connection must be automatically restarted if there is an error. + */ + public static boolean getMustRestartVpn() { + return settings.getBoolean(RESTART_VPN, true); + } + + /** + * Gets if the VPN protection must be activated as soon as possible after booting the OS. + */ + public static boolean getStartOnBoot() { + return settings.getBoolean(START_ON_BOOT, false); + } + + /** + * Gets if the network protection must be activated just after starting the VPN service, which + * would disable the internet connectivity for the rest of the apps while configuring the visor. + */ + public static boolean getProtectBeforeConnected() { + return settings.getBoolean(PROTECT_BEFORE_CONNECTED, true); + } + + ///////////////////////////////////////////////////////////// + // Other operations. + ///////////////////////////////////////////////////////////// + + /** + * Removes the message of the error which caused the VPN service to fail the last time it ran. + */ + public static void removeLastError() { + settings.edit().remove(LAST_ERROR).apply(); + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/vpn/VPNRunnable.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/vpn/VPNRunnable.java new file mode 100644 index 000000000..8eab908b0 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/vpn/VPNRunnable.java @@ -0,0 +1,354 @@ +package com.skywire.skycoin.vpn.vpn; + +import com.skywire.skycoin.vpn.App; +import com.skywire.skycoin.vpn.helpers.HelperFunctions; +import com.skywire.skycoin.vpn.R; + +import java.util.concurrent.TimeUnit; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.ObservableOnSubscribe; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.BehaviorSubject; +import skywiremob.Skywiremob; + +/** + * Class for configuring most of the VPN protection. After creating an instance, the start method + * can be used to start a series of steps for configuring the local visor and creating the VPN + * connection. Each instance can be used one time only, so a new instance must be created for + * starting the VPN protection again. + */ +public class VPNRunnable { + /** + * Current VPN work interface. + */ + private final VPNWorkInterface vpnInterface; + /** + * Object for controlling the local visor. + */ + private VisorRunnable visor; + /** + * Object for connecting the visor with the VPN work interface, to make the VPN functional. + */ + private SkywireVPNConnection vpnConnection; + + /** + * If the procedure to wait for the visor to be available already finished. + */ + private boolean waitAvailableFinished = false; + /** + * If the procedure to wait for having network connectivity already finished. + */ + private boolean waitNetworkFinished = false; + + /** + * If the disconnection procedure already started. + */ + private boolean disconnectionStarted = false; + /** + * Counts how many consecutive times the visor was detected as shut down while disconnecting. + */ + private int disconnectionVerifications = 0; + + /** + * Subject for informing about the state of the VPN protection. + */ + private final BehaviorSubject eventsSubject = BehaviorSubject.create(); + /** + * Subject for informing about the state of the VPN protection. + */ + private Observable eventsObservable; + + /** + * Msg string of the last error detected by this instance. + */ + private String lastErrorMsg; + + private Disposable waitingSubscription; + private Disposable visorTimeoutSubscription; + + /** + * Constructor. + * @param vpnInterface VPN work interface to use. This class will only configure it when + * stabilising the connection, so it will have to be configured before + * using this constructor if the network must be blocked before that. + * Also, this class will not unblock the network after disconnecting, that + * will have to be done by external code. + */ + public VPNRunnable(VPNWorkInterface vpnInterface) { + eventsSubject.onNext(VPNStates.OFF); + this.vpnInterface = vpnInterface; + } + + /** + * Starts the initialization procedure for the VPN protection, if it has not already + * been started. + * @return Observable for knowing the current state of the VPN protection. The operation is not + * started by the subscription, it starts just for calling the function, so there is no need + * for observing in another thread. + */ + public Observable start() { + if (eventsObservable == null) { + // Prepare for sending events. + eventsSubject.onNext(VPNStates.STARTING); + eventsObservable = eventsSubject.hide(); + } + + // Go to the first step. + waitForVisorToBeAvailableIfNeeded(); + + return eventsObservable; + } + + /** + * Allows to know if the initialization failed because the server refused the password. + */ + public boolean getIfPasswordFailed() { + return visor != null ? visor.getIfPasswordFailed() : false; + } + + /** + * Waits for the visor to be totally stopped. After that, goes to the next step for + * starting the VPN protection. If this step was already finished, the function does nothing. + */ + private void waitForVisorToBeAvailableIfNeeded() { + if (!waitAvailableFinished) { + // Avoid having multiple simultaneous procedures. + if (waitingSubscription != null) { + waitingSubscription.dispose(); + } + + // Check if the local visor is not running. If true, continue to the next step. + if (!Skywiremob.isVisorStarting() && !Skywiremob.isVisorRunning()) { + waitAvailableFinished = true; + checkInternetConnectionIfNeeded(true); + } else { + // Update the state. + if (eventsSubject.getValue() != VPNStates.WAITING_PREVIOUS_INSTANCE_STOP) { + Skywiremob.printString("WAITING FOR THE PREVIOUS INSTANCE TO BE FULLY STOPPED"); + eventsSubject.onNext(VPNStates.WAITING_PREVIOUS_INSTANCE_STOP); + } + + // Retry after a delay. + waitingSubscription = Observable.just(0).delay(1000, TimeUnit.MILLISECONDS) + .subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(val -> waitForVisorToBeAvailableIfNeeded()); + } + } + } + + /** + * Waits until there is connection via internet to at least one of the testing URLs set in the + * globals class. After that, goes to the next step for starting the VPN protection. If this + * step was already finished, the function does nothing. + * @param firstTry True if the function is not being called automatically by the function + * itself, to retry the operation. + */ + private void checkInternetConnectionIfNeeded(boolean firstTry) { + if (!waitNetworkFinished) { + Skywiremob.printString("CHECKING CONNECTION"); + + // Update the state. + if (firstTry) { + eventsSubject.onNext(VPNStates.CHECKING_CONNECTIVITY); + } + + // Avoid having multiple simultaneous procedures. + if (waitingSubscription != null) { + waitingSubscription.dispose(); + } + + // Check if there is connection. + waitingSubscription = HelperFunctions.checkInternetConnectivity(firstTry) + .subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(hasInternetConnection -> { + if (hasInternetConnection) { + // Go to the next step. + waitNetworkFinished = true; + startVisorIfNeeded(); + } else { + eventsSubject.onNext(VPNStates.WAITING_FOR_CONNECTIVITY); + waitingSubscription.dispose(); + + // Retry after a delay. + waitingSubscription = Observable.just(0).delay(1000, TimeUnit.MILLISECONDS) + .subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(val -> checkInternetConnectionIfNeeded(false)); + } + }); + } + } + + /** + * Starts the local visor. After that, goes to the next step for starting the VPN protection. + * If this step was already started, the function does nothing. + */ + private void startVisorIfNeeded() { + if (visor == null) { + Skywiremob.printString("STARTING VISOR"); + + // Create the instance for managing the local visor. + visor = new VisorRunnable(); + + if (waitingSubscription != null) { + waitingSubscription.dispose(); + } + + // Start the local visor and listen to the state changes. + waitingSubscription = visor.runVisor() + .subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(state -> { + eventsSubject.onNext(state); + + // Create an observable which stops the operation if there is no progress after + // some time. The observable is reset after each state change. + if (visorTimeoutSubscription != null) { + visorTimeoutSubscription.dispose(); + } + visorTimeoutSubscription = Observable.just(0).delay(45000, TimeUnit.MILLISECONDS) + .subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(val -> { + // Cancel the operation. + HelperFunctions.logError("VPN service", "Timeout preparing the visor."); + putInErrorState(App.getContext().getString(R.string.vpn_timeout_error)); + }); + }, err -> { + // Report the error. + if (visorTimeoutSubscription != null) { + visorTimeoutSubscription.dispose(); + } + putInErrorState(err.getLocalizedMessage()); + }, () -> { + // Go to the next step. + visorTimeoutSubscription.dispose(); + startConnection(); + }); + } + } + + /** + * Starts the VPN connection, which finishes making the VPN protection functional. + */ + private void startConnection() { + if (vpnConnection == null) { + // Create the instance for managing the connection. + vpnConnection = new SkywireVPNConnection(visor, vpnInterface); + + waitingSubscription.dispose(); + + // Make the connection work. Also, check the state changes. + waitingSubscription = vpnConnection.getObservable() + .subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + val -> { + // Inform the state changes. + eventsSubject.onNext(val); + }, err -> { + // Close the connection (this does not means that the network + // will be unblocked) and inform about the error. + putInErrorState(err.getLocalizedMessage()); + }, () -> { + // This event is not expected, but it would mean that the vpn connection + // is not longer active. + HelperFunctions.logError("VPN connection ended unexpectedly", "VPN connection ended unexpectedly"); + disconnect(); + } + ); + } + } + + /** + * Reverts all the steps made by this class, which means closing the connection and stopping + * the visor. If the network connections were blocked, that does not change, as this function + * does not make changes to the VPN work interface. Calling this function again after the + * first call does nothing. + */ + public void disconnect() { + if (!disconnectionStarted) { + disconnectionStarted = true; + + Skywiremob.printString("DISCONNECTING VPN RUNNABLE"); + + // Inform the new state. + eventsSubject.onNext(VPNStates.DISCONNECTING); + + // Remove the subscriptions and close the vpn connection. + if (waitingSubscription != null) { + waitingSubscription.dispose(); + } + if (visorTimeoutSubscription != null) { + visorTimeoutSubscription.dispose(); + } + if (this.vpnConnection != null) { + this.vpnConnection.close(); + } + + // Stop the visor in another thread. + Observable.create((ObservableOnSubscribe) emitter -> { + if (visor != null) { + visor.startStoppingVisor(); + } + emitter.onComplete(); + }).subscribeOn(Schedulers.newThread()).subscribe(val -> {}); + + // Wait until the visor is completely stopped. 2 consecutive checks must be passed, + // to avoid a very unlikely but possible race condition. + Observable.timer(100, TimeUnit.MILLISECONDS).repeatUntil(() -> { + if (!Skywiremob.isVisorStarting() && !Skywiremob.isVisorRunning()) { + if (disconnectionVerifications == 2) { + return true; + } else { + disconnectionVerifications += 1; + } + } else { + if (disconnectionVerifications != 0) { + if (visor != null) { + visor.startStoppingVisor(); + } + } + + disconnectionVerifications = 0; + } + + return false; + }) + .subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(val -> {}, err -> {}, () -> eventsSubject.onNext(VPNStates.DISCONNECTED)); + } + } + + /** + * Informs about an error and closes the VPN connection. + * @param errorMsg Msg string of the error. + */ + private void putInErrorState(String errorMsg) { + lastErrorMsg = errorMsg; + + // If the network is already blocked and the kill switch is active, inform that the + // current error will close the VPN connection but the network will still be blocked until + // the user stops the service manually. That behavior is not managed by this class. + if (!vpnInterface.alreadyConfigured() || !VPNGeneralPersistentData.getKillSwitchActivated()) { + eventsSubject.onNext(VPNStates.ERROR); + } else { + eventsSubject.onNext(VPNStates.BLOCKING_ERROR); + } + + disconnect(); + } + + /** + * Returns the msg of the last error detected by the current instance. + */ + public String getLastErrorMsg() { + return lastErrorMsg; + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/vpn/VPNServersPersistentData.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/vpn/VPNServersPersistentData.java new file mode 100644 index 000000000..3aaf0d4fc --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/vpn/VPNServersPersistentData.java @@ -0,0 +1,322 @@ +package com.skywire.skycoin.vpn.vpn; + +import android.content.SharedPreferences; + +import androidx.preference.PreferenceManager; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.skywire.skycoin.vpn.App; +import com.skywire.skycoin.vpn.activities.servers.VpnServerForList; +import com.skywire.skycoin.vpn.objects.LocalServerData; +import com.skywire.skycoin.vpn.objects.ManualVpnServerData; +import com.skywire.skycoin.vpn.objects.ServerFlags; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.subjects.ReplaySubject; + +/** + * Helper class for saving and getting data related to the VPN servers to and from the + * persistent storage. + */ +public class VPNServersPersistentData { + /** + * Singleton instance. + */ + private static final VPNServersPersistentData instance = new VPNServersPersistentData(); + /** + * Gets the singleton for using the class. + */ + public static VPNServersPersistentData getInstance() { return instance; } + + private final int maxHistoryElements = 30; + + // Keys for persistent storage. + private final String CURRENT_SERVER_PK = "serverPK"; + private final String SERVER_LIST = "serverList"; + + private SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(App.getContext()); + + private String currentServerPk; + private HashMap serversMap; + + private ReplaySubject currentServerSubject = ReplaySubject.createWithSize(1); + private ReplaySubject> historySubject = ReplaySubject.createWithSize(1); + private ReplaySubject> favoritesSubject = ReplaySubject.createWithSize(1); + private ReplaySubject> blockedSubject = ReplaySubject.createWithSize(1); + + private VPNServersPersistentData() { + currentServerPk = settings.getString(CURRENT_SERVER_PK, ""); + + String serversList = settings.getString(SERVER_LIST, null); + if (serversList != null) { + Gson gson = new Gson(); + Type mapType = new TypeToken>() {}.getType(); + serversMap = gson.fromJson(serversList, mapType); + + LocalServerData currentServer = this.serversMap.get(currentServerPk); + this.currentServerSubject.onNext(currentServer != null ? currentServer : new LocalServerData()); + } else { + serversMap = new HashMap<>(); + this.currentServerSubject.onNext(new LocalServerData()); + } + + this.launchListEvents(); + } + + public LocalServerData getCurrentServer() { + return serversMap.get(this.currentServerPk); + } + + public Observable getCurrentServerObservable() { + return currentServerSubject.hide(); + } + + public Observable> history() { + return this.historySubject.hide(); + } + + public Observable> favorites() { + return this.favoritesSubject.hide(); + } + + public Observable> blocked() { + return this.blockedSubject.hide(); + } + + public LocalServerData getSavedVersion(String pk) { + return this.serversMap.get(pk); + } + + public void updateFromDiscovery(ArrayList serverList) { + for (VpnServerForList server : serverList) { + if (this.serversMap.containsKey(server.pk)) { + LocalServerData savedServer = this.serversMap.get(server.pk); + + savedServer.countryCode = server.countryCode; + savedServer.name = server.name; + savedServer.location = server.location; + savedServer.note = server.note; + } + } + + this.saveData(); + } + + public void updateServer(LocalServerData server) { + this.serversMap.put(server.pk, server); + this.cleanServers(); + this.saveData(); + } + + public LocalServerData processFromList(VpnServerForList newServer) { + LocalServerData retrievedServer = this.serversMap.get(newServer.pk); + if (retrievedServer != null) { + retrievedServer.countryCode = newServer.countryCode; + retrievedServer.name = newServer.name; + retrievedServer.location = newServer.location; + retrievedServer.note = newServer.note; + + this.saveData(); + + return retrievedServer; + } + + LocalServerData response = new LocalServerData(); + response.countryCode = newServer.countryCode; + response.name = newServer.name; + response.customName = null; + response.pk = newServer.pk; + response.lastUsed = new Date(0); + response.inHistory = false; + response.flag = ServerFlags.None; + response.location = newServer.location; + response.personalNote = null; + response.note = newServer.note; + response.enteredManually = false; + response.password = null; + + return response; + } + + public LocalServerData processFromManual(ManualVpnServerData newServer) { + LocalServerData retrievedServer = this.serversMap.get(newServer.pk); + if (retrievedServer != null) { + retrievedServer.password = newServer.password; + retrievedServer.customName = newServer.name; + retrievedServer.personalNote = newServer.note; + retrievedServer.enteredManually = true; + + this.saveData(); + + return retrievedServer; + } + + LocalServerData response = new LocalServerData(); + response.countryCode = "zz"; + response.name = null; + response.customName = newServer.name; + response.pk = newServer.pk; + response.lastUsed = new Date(0); + response.inHistory = false; + response.flag = ServerFlags.None; + response.location = null; + response.personalNote = newServer.note; + response.note = null; + response.enteredManually = true; + response.password = newServer.password; + + return response; + } + + public void changeFlag(LocalServerData server, ServerFlags flag) { + LocalServerData retrievedServer = this.serversMap.get(server.pk); + if (retrievedServer != null) { + server = retrievedServer; + } + + if (server.flag == flag) { + return; + } + server.flag = flag; + + if (!this.serversMap.containsKey(server.pk)) { + this.serversMap.put(server.pk, server); + } + + this.cleanServers(); + this.saveData(); + } + + public void removePassword(String pk) { + LocalServerData retrievedServer = this.serversMap.get(pk); + if (retrievedServer == null || retrievedServer.password == null || retrievedServer.password.equals("")) { + return; + } + + retrievedServer.password = null; + this.cleanServers(); + this.saveData(); + } + + public void removeFromHistory(String pk) { + LocalServerData retrievedServer = this.serversMap.get(pk); + if (retrievedServer == null || !retrievedServer.inHistory) { + return; + } + + retrievedServer.inHistory = false; + this.cleanServers(); + this.saveData(); + } + + public void modifyCurrentServer(LocalServerData newServer) { + if (!this.serversMap.containsKey(newServer.pk)) { + this.serversMap.put(newServer.pk, newServer); + } + + this.currentServerPk = newServer.pk; + + LocalServerData currentServer = this.serversMap.get(currentServerPk); + this.currentServerSubject.onNext(currentServer); + + this.cleanServers(); + this.saveData(); + } + + public void updateHistory() { + LocalServerData currentServer = this.serversMap.get(currentServerPk); + // This should not happen. + if (currentServer == null) { + return; + } + + currentServer.lastUsed = new Date(); + currentServer.inHistory = true; + + // Make a list with the servers in the history and sort it by usage date. + ArrayList historyList = new ArrayList(); + for (LocalServerData server : serversMap.values()) { + if (server.inHistory) { + historyList.add(server); + } + } + Comparator comparator = (a, b) -> (int)((b.lastUsed.getTime() - a.lastUsed.getTime()) / 1000); + Collections.sort(historyList, comparator); + + // Remove from the history the old servers. + int historyElementsFound = 0; + for (LocalServerData server : historyList) { + if (historyElementsFound < this.maxHistoryElements) { + historyElementsFound += 1; + } else { + server.inHistory = false; + } + } + + this.cleanServers(); + this.saveData(); + } + + private void cleanServers() { + ArrayList unneeded = new ArrayList(); + for (LocalServerData server : serversMap.values()) { + if ( + !server.inHistory && + server.flag == ServerFlags.None && + !server.pk.equals(this.currentServerPk) && + (server.customName == null || server.customName.equals("")) && + (server.personalNote == null || server.personalNote.equals("")) + ) { + unneeded.add(server.pk); + } + } + + for (String pk : unneeded) { + this.serversMap.remove(pk); + } + } + + private void saveData() { + Gson gson = new Gson(); + String servers = gson.toJson(serversMap); + + settings + .edit() + .putString(SERVER_LIST, servers) + .putString(CURRENT_SERVER_PK, currentServerPk) + .apply(); + + this.launchListEvents(); + } + + private void launchListEvents() { + ArrayList history = new ArrayList(); + ArrayList favorites = new ArrayList(); + ArrayList blocked = new ArrayList(); + + for (LocalServerData server : serversMap.values()) { + if (server.inHistory) { + history.add(server); + } + if (server.flag == ServerFlags.Favorite) { + favorites.add(server); + } + if (server.flag == ServerFlags.Blocked) { + blocked.add(server); + } + } + + this.historySubject.onNext(history); + this.favoritesSubject.onNext(favorites); + this.blockedSubject.onNext(blocked); + this.currentServerSubject.onNext(currentServerSubject.getValue()); + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/vpn/VPNStates.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/vpn/VPNStates.java new file mode 100644 index 000000000..dd50fbc1c --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/vpn/VPNStates.java @@ -0,0 +1,267 @@ +package com.skywire.skycoin.vpn.vpn; + +import com.skywire.skycoin.vpn.R; + +import java.util.HashMap; + +/** + * Helper class with the possible states of the VPN service. + * + * The states are numeric constants, similar to how http status codes work, to be able to identify + * state groups just by numeric ranges. The ranges are: + * + * State < 10: the service is not running. + * + * 10 =< State < 100: The VPN connection is being prepared. + * + * 100 =< State < 150: The VPN connection has been made and the internet connectivity should + * be protected and working. + * + * 150 =< State < 200: Temporal errors with the VPN connection. + * + * 200 =< State < 300: Closing the VPN connection/service. + * + * 300 =< State < 400: VPN connection/service closed. + * + * State >= 400 : An error occurred. + */ +public enum VPNStates { + /** + * The service is off. + */ + OFF(1), + /** + * Starting the service. + */ + STARTING(10), + /** + * Waiting for the visor to be completely stopped before starting it again. + */ + WAITING_PREVIOUS_INSTANCE_STOP(12), + /** + * Checking for the first time if the device has internet connectivity. + */ + CHECKING_CONNECTIVITY(15), + /** + * No internet connectivity was found and the service is checking again periodically. + */ + WAITING_FOR_CONNECTIVITY(16), + /** + * Starting the Skywire visor. + */ + PREPARING_VISOR(20), + /** + * Starting the VPN client, which is part of Skywiremob and running as part of the visor. + */ + PREPARING_VPN_CLIENT(30), + /** + * Making final preparations for the VPN client, like performing the handshake and start serving. + */ + FINAL_PREPARATIONS_FOR_VISOR(35), + /** + * The visor and VPN client are ready. Preparations may be needed in the app side. + */ + VISOR_READY(40), + /** + * The VPN connection has been fully established and secure internet connectivity should + * be available. + */ + CONNECTED(100), + /** + * There was an error with the VPN connection and it is being restored automatically. + */ + RESTORING_VPN(150), + /** + * There was an error and the whole VPN service is being restored automatically. + */ + RESTORING_SERVICE(155), + /** + * The VPN service is being stopped. + */ + DISCONNECTING(200), + /** + * The VPN service has been stopped. + */ + DISCONNECTED(300), + /** + * There has been an error, the VPN connection is not available and the service is + * being stopped. + */ + ERROR(400), + /** + * There has been and error and the VPN connection is not available. The network will remain + * blocked until the user stops the service manually. + */ + BLOCKING_ERROR(410); + + /** + * Allows to easily get the value related to an specific number. + */ + private static HashMap numericValues; + + // Initializes the enum and saves the value. + private final int val; + VPNStates(int val) { + this.val = val; + } + + /** + * Gets the associated numeric value. + */ + public int val() { + return val; + } + + /** + * Class with details about the state of the VPN service. + */ + public static class StateInfo { + /** + * Current state of the service. + */ + public final VPNStates state; + /** + * If the service was started by the OS, which means that the OS is responsible for + * stopping it. + */ + public final boolean startedByTheSystem; + /** + * If the user already requested the service to be stopped. + */ + public final boolean stopRequested; + + public StateInfo(VPNStates state, boolean startedByTheSystem, boolean stopRequested) { + this.state = state; + this.startedByTheSystem = startedByTheSystem; + this.stopRequested = stopRequested; + } + } + + /** + * Allows to get the resource ID of the string with the title for a state of the + * VPN service. If no resource is found for the state, -1 is returned. + */ + public static int getTitleForState(VPNStates state) { + if (state == OFF) { + return R.string.vpn_state_disconnected; + } else if (state == STARTING) { + return R.string.vpn_state_connecting; + } else if (state == WAITING_PREVIOUS_INSTANCE_STOP) { + return R.string.vpn_state_connecting; + } else if (state == CHECKING_CONNECTIVITY) { + return R.string.vpn_state_connecting; + } else if (state == WAITING_FOR_CONNECTIVITY) { + return R.string.vpn_state_connecting; + } else if (state == PREPARING_VISOR) { + return R.string.vpn_state_connecting; + } else if (state == PREPARING_VPN_CLIENT) { + return R.string.vpn_state_connecting; + } else if (state == FINAL_PREPARATIONS_FOR_VISOR) { + return R.string.vpn_state_connecting; + } else if (state == VISOR_READY) { + return R.string.vpn_state_connecting; + } else if (state == CONNECTED) { + return R.string.vpn_state_connected; + } else if (state == RESTORING_VPN) { + return R.string.vpn_state_restarting; + } else if (state == RESTORING_SERVICE) { + return R.string.vpn_state_restarting; + } else if (state == DISCONNECTING) { + return R.string.vpn_state_disconnecting; + } else if (state == DISCONNECTED) { + return R.string.vpn_state_disconnected; + } else if (state == ERROR) { + return R.string.vpn_state_error; + } else if (state == BLOCKING_ERROR) { + return R.string.vpn_state_error; + } + + return -1; + } + + /** + * Allows to get the resource ID of the color for the title of a state of the + * VPN service. If no resource is found for the title, red is returned. + */ + public static int getColorForStateTitle(int titleResource) { + if (titleResource == R.string.vpn_state_disconnected) { + return R.color.red; + } else if (titleResource == R.string.vpn_state_connecting) { + return R.color.yellow; + } else if (titleResource == R.string.vpn_state_connected) { + return R.color.green; + } else if (titleResource == R.string.vpn_state_restarting) { + return R.color.yellow; + } else if (titleResource == R.string.vpn_state_disconnecting) { + return R.color.yellow; + } else if (titleResource == R.string.vpn_state_error) { + return R.color.red; + } + + return R.color.red; + } + + /** + * Allows to get the resource ID of the string with the description of a state of the + * VPN service. If no resource is found for the state, -1 is returned. + */ + public static int getDescriptionForState(VPNStates state) { + if (state == OFF) { + return R.string.vpn_state_details_off; + } else if (state == STARTING) { + return R.string.vpn_state_details_initializing; + } else if (state == WAITING_PREVIOUS_INSTANCE_STOP) { + return R.string.vpn_state_details_waiting_previous_instance_stop; + } else if (state == CHECKING_CONNECTIVITY) { + return R.string.vpn_state_details_checking_connectivity; + } else if (state == WAITING_FOR_CONNECTIVITY) { + return R.string.vpn_state_details_waiting_connectivity; + } else if (state == PREPARING_VISOR) { + return R.string.vpn_state_details_starting_visor; + } else if (state == PREPARING_VPN_CLIENT) { + return R.string.vpn_state_details_starting_vpn_app; + } else if (state == FINAL_PREPARATIONS_FOR_VISOR) { + return R.string.vpn_state_details_additional_visor_initializations; + } else if (state == VISOR_READY) { + return R.string.vpn_state_details_connecting; + } else if (state == CONNECTED) { + return R.string.vpn_state_details_connected; + } else if (state == RESTORING_VPN) { + return R.string.vpn_state_details_restoring; + } else if (state == RESTORING_SERVICE) { + return R.string.vpn_state_details_restoring_service; + } else if (state == DISCONNECTING) { + return R.string.vpn_state_details_disconnecting; + } else if (state == DISCONNECTED) { + return R.string.vpn_state_details_disconnected; + } else if (state == ERROR) { + return R.string.vpn_state_details_error; + } else if (state == BLOCKING_ERROR) { + return R.string.vpn_state_details_blocking_error; + } + + return -1; + } + + /** + * Allows to get the value associated with a numeric value. If there is no value for the + * provided number, the OFF state is returned. + * @param value Value to check. + */ + public static VPNStates valueOf(int value) { + // Initialize the map for getting the values, if needed. + if (numericValues == null) { + numericValues = new HashMap<>(); + + for (VPNStates v : VPNStates.values()) { + numericValues.put(v.val(), v); + } + } + + if (!numericValues.containsKey(value)) { + return OFF; + } + + return numericValues.get(value); + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/vpn/VPNWorkInterface.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/vpn/VPNWorkInterface.java new file mode 100644 index 000000000..d611cb0dc --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/vpn/VPNWorkInterface.java @@ -0,0 +1,242 @@ +package com.skywire.skycoin.vpn.vpn; + +import android.net.VpnService; +import android.os.ParcelFileDescriptor; + +import com.skywire.skycoin.vpn.App; +import com.skywire.skycoin.vpn.helpers.Globals; +import com.skywire.skycoin.vpn.helpers.HelperFunctions; +import com.skywire.skycoin.vpn.R; + +import java.io.Closeable; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.HashSet; + +import skywiremob.Skywiremob; + +/** + * Object used for starting the VPN protection and sending/receiving data. After created, to start + * the VPN protection the object must be configured. + */ +public class VPNWorkInterface implements Closeable { + /** + * Modes in which the VPN interface can be configured. + */ + public enum Modes { + /** + * Used just for blocking the network connectivity before configuring the visor, to avoid + * data leaks. + */ + BLOCKING, + /** + * Normal mode for sending and receiving data using the VPN protection. + */ + WORKING, + /** + * Mode used just for configuring a VPN interface and closing it immediately after that, to + * force the OS to disable the VPN protection, due to a bug in old Android versions. + */ + DELETING, + } + + /** + * Current VPN service instance. + */ + private final VpnService service; + /** + * Current VPN communication object, created by the system. + */ + private ParcelFileDescriptor vpnInterface = null; + /** + * Input stream to be used with the current communication object created by the system. + */ + private FileInputStream inStream = null; + /** + * Output stream to be used with the current communication object created by the system. + */ + private FileOutputStream outStream = null; + + public VPNWorkInterface(VpnService service) { + this.service = service; + } + + /** + * Terminates the VPN protections and cleans the used resources. + */ + @Override + public void close() { + if (vpnInterface != null) { + try { + vpnInterface.close(); + vpnInterface = null; + } catch (IOException e) { + HelperFunctions.logError("Unable to close interface", e); + } + + cleanInputStream(); + cleanOutputStream(); + } + } + + /** + * Checks if the interface has already been configured for the first time. + */ + public boolean alreadyConfigured() { + return vpnInterface != null; + } + + /** + * Configures and activates the VPN interface. After calling this function the OS starts + * routing the data using the interface, so all network connections will be blocked if the VPN + * is not working properly. This method can be called several times, which allows to restore + * the connection in case of errors or change the mode. + * @param mode Mode in which the VPN interface will be configured. + */ + public void configure(Modes mode) throws Exception { + // Save a reference to the current interface, if any, to close it after creating the + // new one, to avoid leaking data while the new interface is created. + ParcelFileDescriptor oldVpnInterface = null; + if (vpnInterface != null) { + oldVpnInterface = vpnInterface; + } + + // Create and configure a builder. + VpnService.Builder builder = service.new Builder(); + builder.setMtu((short)Skywiremob.getMTU()); + if (mode == Modes.WORKING) { + Skywiremob.printString("TUN IP: " + Skywiremob.tunip()); + // Get the address from the visor. + builder.addAddress(Skywiremob.tunip(), (int) Skywiremob.getTUNIPPrefix()); + } else { + // Use an address for blocking all connections. + builder.addAddress("8.8.8.8", 32); + } + + // Use the custom DNS server, if any. + String dnsServer = VPNGeneralPersistentData.getCustomDns(); + if (dnsServer != null && dnsServer.trim().length() > 0) { + builder.addDnsServer(dnsServer.trim()); + } + + builder.addRoute("0.0.0.0", 0); + // This makes the streams created with the interface synchronous, so that the data can be + // read blocking an independent thread in an efficient way. + builder.setBlocking(true); + + // Allows to know if there was an error allowing or disallowing apps. + boolean errorIgnoringApps = false; + + if (mode == Modes.WORKING || mode == Modes.BLOCKING) { + String upperCaseAppPackage = App.getContext().getPackageName().toUpperCase(); + Globals.AppFilteringModes appsSelectionMode = VPNGeneralPersistentData.getAppsSelectionMode(); + + if (appsSelectionMode != Globals.AppFilteringModes.PROTECT_ALL) { + // Get the package name of all the apps selected by the user which are + // currently installed. + for (String packageName : HelperFunctions.filterAvailableApps(VPNGeneralPersistentData.getAppList(new HashSet<>()))) { + try { + if (appsSelectionMode == Globals.AppFilteringModes.PROTECT_SELECTED) { + // Protect all selected apps, but ignore this app. + if (!upperCaseAppPackage.equals(packageName.toUpperCase())) { + builder.addAllowedApplication(packageName); + } + } else { + // Avoid protecting the selected apps, but ignore this app. + if (!upperCaseAppPackage.equals(packageName.toUpperCase())) { + builder.addDisallowedApplication(packageName); + } + } + } catch (Exception e) { + errorIgnoringApps = true; + HelperFunctions.logError("Unable to add " + packageName + " to the VPN service", e); + break; + } + } + } + + // Make the VPN protection ignore this app, as free access is needed for configuring + // the visor, specially in case of errors, when it is needed to restart components. + if (!errorIgnoringApps) { + try { + if (appsSelectionMode != Globals.AppFilteringModes.PROTECT_SELECTED) { + builder.addDisallowedApplication(App.getContext().getPackageName()); + } + } catch (Exception e) { + errorIgnoringApps = true; + HelperFunctions.logError("Unable to add VPN app rule to the VPN service", e); + } + } + } else { + // Block this app only, to be able to avoid a bug in old Android versions. + builder.addAllowedApplication(App.getContext().getPackageName()); + } + + if (errorIgnoringApps) { + throw new Exception(App.getContext().getString(R.string.vpn_service_configuring_app_rules_error)); + } + + // Create the new interface using the builder. + builder.setConfigureIntent(HelperFunctions.getOpenAppPendingIntent()); + synchronized (service) { + vpnInterface = builder.establish(); + } + Skywiremob.printString("New interface: " + vpnInterface); + + // Close the previous interface and streams, if any. + if (oldVpnInterface != null) { + oldVpnInterface.close(); + } + cleanInputStream(); + cleanOutputStream(); + } + + /** + * Gets the input stream for reading the packages from the system that must be sent using the + * VPN. NOTE: if the interface is closed or configured again, the stream is closed. + */ + public FileInputStream getInputStream() { + if (inStream == null) { + inStream = new FileInputStream(vpnInterface.getFileDescriptor()); + } + return inStream; + } + + /** + * Gets the output stream that must be used for sending to the system the packages received via + * the VPN. NOTE: if the interface is closed or configured again, the stream is closed. + */ + public FileOutputStream getOutputStream() { + if (outStream == null) { + outStream = new FileOutputStream(vpnInterface.getFileDescriptor()); + } + return outStream; + } + + /** + * Cleans and removes the current input stream, if any. + */ + private void cleanInputStream() { + if (inStream != null) { + try { + inStream.close(); + } catch (Exception e) { } + + inStream = null; + } + } + + /** + * Cleans and removes the current output stream, if any. + */ + private void cleanOutputStream() { + if (outStream != null) { + try { + outStream.close(); + } catch (Exception e) { } + + outStream = null; + } + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/vpn/VisorRunnable.java b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/vpn/VisorRunnable.java new file mode 100644 index 000000000..a5614e533 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/java/com/skywire/skycoin/vpn/vpn/VisorRunnable.java @@ -0,0 +1,218 @@ +package com.skywire.skycoin.vpn.vpn; + +import com.skywire.skycoin.vpn.App; +import com.skywire.skycoin.vpn.R; +import com.skywire.skycoin.vpn.helpers.HelperFunctions; +import com.skywire.skycoin.vpn.objects.LocalServerData; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.ObservableEmitter; +import io.reactivex.rxjava3.core.ObservableOnSubscribe; +import skywiremob.Skywiremob; + +/** + * Allows to easily control the starting and stopping procedures of the the visor and VPN client + * included in Skywiremob. + */ +public class VisorRunnable { + /** + * If Skywiremob.prepareVPNClient has already been called without errors. + */ + private boolean vpnClientStarted = false; + /** + * If Skywiremob.startListeningUDP() has already been called without errors. + */ + private boolean listeningUdp = false; + /** + * If true, the initialization failed because the server refused the password. + */ + private boolean passwordFailed = false; + + /** + * Allows to know if the initialization failed because the server refused the password. + */ + public boolean getIfPasswordFailed() { + return passwordFailed; + } + + /** + * Starts stopping the visor. It returns before the visor has been completely stopped. + */ + public void startStoppingVisor() { + skywiremob.Error err = Skywiremob.stopVisor(); + if (err.getCode() != Skywiremob.ErrCodeNoError) { + Skywiremob.printString(gerErrorMsg(err)); + HelperFunctions.showToast(gerErrorMsg(err), false); + } + Skywiremob.printString("Visor stopped"); + } + + /** + * Stops the VPN client without stopping the visor. + */ + public void stopVpnConnection() { + if (vpnClientStarted) { + Skywiremob.stopVPNClient(); + vpnClientStarted = false; + } + if (listeningUdp) { + Skywiremob.stopListeningUDP(); + listeningUdp = false; + } + Skywiremob.printString("VPN connection stopped"); + } + + /** + * Starts the Skywire visor. + * @return Observable that will emit the current state of the process, as variables defined in + * VPNStates, and will complete after starting the visor. + */ + public Observable runVisor() { + return Observable.create((ObservableOnSubscribe) emitter -> { + if (emitter.isDisposed()) { return; } + emitter.onNext(VPNStates.PREPARING_VISOR); + + // Start the visor if the emitter is still valid. + if (emitter.isDisposed()) { return; } + skywiremob.Error err = Skywiremob.prepareVisor(); + if (err.getCode() != Skywiremob.ErrCodeNoError) { + HelperFunctions.logError("Visor startup procedure, code " + err.getCode(), gerErrorMsg(err)); + if (emitter.isDisposed()) { return; } + emitter.onError(new Exception(gerErrorMsg(err))); + return; + } + + // Block the thread while the visor is starting. + err = Skywiremob.waitVisorReady(); + if (err.getCode() != Skywiremob.ErrCodeNoError) { + HelperFunctions.logError("Visor startup procedure, code " + err.getCode(), gerErrorMsg(err)); + if (emitter.isDisposed()) { return; } + emitter.onError(new Exception(gerErrorMsg(err))); + return; + } + + // Finish. + Skywiremob.printString("Prepared visor"); + if (emitter.isDisposed()) { return; } + emitter.onNext(VPNStates.VISOR_READY); + emitter.onComplete(); + }); + } + + /** + * Starts the VPN client. This function was made to be used inside an observable which emits + * the state of the VPN service. + * @param parentEmitter Emitter of the observable from which this function was called, to be + * able to emit the state changes. + */ + public void runVpnClient(ObservableEmitter parentEmitter) throws Exception { + passwordFailed = false; + + // Update the state. + if (parentEmitter.isDisposed()) { return; } + parentEmitter.onNext(VPNStates.PREPARING_VPN_CLIENT); + + // Prepare the VPN client with the last saved public key and password. + if (parentEmitter.isDisposed()) { return; } + LocalServerData currentServer = VPNServersPersistentData.getInstance().getCurrentServer(); + String savedPk = currentServer != null ? currentServer.pk : ""; + String savedPassword = currentServer != null && currentServer.password != null ? currentServer.password : ""; + skywiremob.Error err = Skywiremob.prepareVPNClient(savedPk, savedPassword); + if (err.getCode() != Skywiremob.ErrCodeNoError) { + throw new Exception(gerErrorMsg(err)); + } + vpnClientStarted = true; + Skywiremob.printString("Prepared VPN client"); + if (parentEmitter.isDisposed()) { return; } + parentEmitter.onNext(VPNStates.FINAL_PREPARATIONS_FOR_VISOR); + + // Perform the handshake. + if (parentEmitter.isDisposed()) { return; } + err = Skywiremob.shakeHands(); + if (err.getCode() != Skywiremob.ErrCodeNoError) { + // Check if the server refused the password. + if (err.getCode() == Skywiremob.ErrCodeHandshakeFailed && err.getError().toUpperCase().contains("4 (Forbidden)".toUpperCase())) { + passwordFailed = true; + } + throw new Exception(gerErrorMsg(err)); + } + + // Start listening. + if (parentEmitter.isDisposed()) { return; } + err = Skywiremob.startListeningUDP(); + listeningUdp = true; + if (err.getCode() != Skywiremob.ErrCodeNoError) { + throw new Exception(gerErrorMsg(err)); + } + + // Start serving. + if (parentEmitter.isDisposed()) { return; } + err = Skywiremob.serveVPN(); + if (err.getCode() != Skywiremob.ErrCodeNoError) { + throw new Exception(gerErrorMsg(err)); + } + } + + /** + * Gets the error string for an specific error returned by Skywiremob. + */ + private static String gerErrorMsg(skywiremob.Error error) { + int resource = -1; + + if (error.getCode() == Skywiremob.ErrCodeInvalidPK) { + resource = R.string.skywiremob_error_invalid_pk; + } else if (error.getCode() == Skywiremob.ErrCodeInvalidVisorConfig) { + resource = R.string.skywiremob_error_invalid_visor_config; + } else if (error.getCode() == Skywiremob.ErrCodeInvalidAddrResolverURL) { + resource = R.string.skywiremob_error_invalid_addr_resolver_url; + } else if (error.getCode() == Skywiremob.ErrCodeSTCPInitFailed) { + resource = R.string.skywiremob_error_stcp_init_failed; + } else if (error.getCode() == Skywiremob.ErrCodeSTCPRInitFailed) { + resource = R.string.skywiremob_error_stcpr_init_failed; + } else if (error.getCode() == Skywiremob.ErrCodeSUDPHInitFailed) { + resource = R.string.skywiremob_error_sudph_init_failed; + } else if (error.getCode() == Skywiremob.ErrCodeDmsgListenFailed) { + resource = R.string.skywiremob_error_dmsg_listen_failed; + } else if (error.getCode() == Skywiremob.ErrCodeTpDiscUnavailable) { + resource = R.string.skywiremob_error_tp_disc_unavailable; + } else if (error.getCode() == Skywiremob.ErrCodeFailedToStartRouter) { + resource = R.string.skywiremob_error_failed_to_start_router; + } else if (error.getCode() == Skywiremob.ErrCodeFailedToSetupHVGateway) { + resource = R.string.skywiremob_error_failed_to_setup_hv_gateway; + } else if (error.getCode() == Skywiremob.ErrCodeVisorNotRunning) { + resource = R.string.skywiremob_error_visor_not_running; + } else if (error.getCode() == Skywiremob.ErrCodeInvalidRemotePK) { + resource = R.string.skywiremob_error_invalid_remote_pk; + } else if (error.getCode() == Skywiremob.ErrCodeFailedToSaveTransport) { + resource = R.string.skywiremob_error_failed_to_save_transport; + } else if (error.getCode() == Skywiremob.ErrCodeVPNServerUnavailable) { + resource = R.string.skywiremob_error_vpn_server_unavailable; + } else if (error.getCode() == Skywiremob.ErrCodeVPNClientNotRunning) { + resource = R.string.skywiremob_error_vpn_client_not_running; + } else if (error.getCode() == Skywiremob.ErrCodeHandshakeFailed) { + if (error.getError().toUpperCase().contains("4 (Forbidden)".toUpperCase())) { + resource = R.string.skywiremob_error_wrong_password; + } else { + resource = R.string.skywiremob_error_handshake_failed; + } + } else if (error.getCode() == Skywiremob.ErrCodeInvalidAddr) { + resource = R.string.skywiremob_error_invalid_addr; + } else if (error.getCode() == Skywiremob.ErrCodeAlreadyListeningUDP) { + resource = R.string.skywiremob_error_already_listening_udp; + } else if (error.getCode() == Skywiremob.ErrCodeUDPListenFailed) { + resource = R.string.skywiremob_error_udp_listen_failed; + } + + String response; + if (resource != -1) { + response = App.getContext().getString(resource); + } else { + response = error.getError(); + if (response == null || response.trim().equals("")) { + response = App.getContext().getString(R.string.skywiremob_error_unknown); + } + } + + return response; + } +} diff --git a/cmd/skywirevisormobile/android/app/src/main/res/animator/anim_start_button.xml b/cmd/skywirevisormobile/android/app/src/main/res/animator/anim_start_button.xml new file mode 100644 index 000000000..582fa05dd --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/res/animator/anim_start_button.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/cmd/skywirevisormobile/android/app/src/main/res/animator/anim_state.xml b/cmd/skywirevisormobile/android/app/src/main/res/animator/anim_state.xml new file mode 100644 index 000000000..103fd5037 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/res/animator/anim_state.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-hdpi/bronze_rating.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-hdpi/bronze_rating.png new file mode 100644 index 000000000..69db4edda Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-hdpi/bronze_rating.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-hdpi/gold_rating.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-hdpi/gold_rating.png new file mode 100644 index 000000000..59c43cd71 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-hdpi/gold_rating.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-hdpi/modal_background_pattern.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-hdpi/modal_background_pattern.png new file mode 100644 index 000000000..33f259744 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-hdpi/modal_background_pattern.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-hdpi/silver_rating.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-hdpi/silver_rating.png new file mode 100644 index 000000000..89a0a368f Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-hdpi/silver_rating.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xhdpi/background_box1.9.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xhdpi/background_box1.9.png new file mode 100644 index 000000000..6acb77df2 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xhdpi/background_box1.9.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xhdpi/background_box2.9.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xhdpi/background_box2.9.png new file mode 100644 index 000000000..d8d214fe7 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xhdpi/background_box2.9.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xhdpi/background_box3.9.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xhdpi/background_box3.9.png new file mode 100644 index 000000000..8324a2152 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xhdpi/background_box3.9.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xhdpi/background_box4.9.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xhdpi/background_box4.9.png new file mode 100644 index 000000000..ced82888a Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xhdpi/background_box4.9.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xhdpi/background_box5.9.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xhdpi/background_box5.9.png new file mode 100644 index 000000000..6d1f22327 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xhdpi/background_box5.9.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xhdpi/box_pattern.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xhdpi/box_pattern.png new file mode 100644 index 000000000..1a838ed16 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xhdpi/box_pattern.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xhdpi/logo_vpn.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xhdpi/logo_vpn.png new file mode 100644 index 000000000..22000faeb Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xhdpi/logo_vpn.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xhdpi/map_phones.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xhdpi/map_phones.png new file mode 100644 index 000000000..1218ddbc9 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xhdpi/map_phones.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xhdpi/red_btn.9.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xhdpi/red_btn.9.png new file mode 100644 index 000000000..ffb342a49 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xhdpi/red_btn.9.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xhdpi/select_arrow.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xhdpi/select_arrow.png new file mode 100644 index 000000000..7a15f38a0 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xhdpi/select_arrow.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xhdpi/start_btn.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xhdpi/start_btn.png new file mode 100644 index 000000000..0c92d539d Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xhdpi/start_btn.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ab.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ab.png new file mode 100644 index 000000000..82ffb169a Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ab.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ad.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ad.png new file mode 100644 index 000000000..7b12b7214 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ad.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ae.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ae.png new file mode 100644 index 000000000..9e316d905 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ae.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/af.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/af.png new file mode 100644 index 000000000..7879ab7af Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/af.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ag.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ag.png new file mode 100644 index 000000000..48e08fb9c Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ag.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ai.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ai.png new file mode 100644 index 000000000..611293b97 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ai.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/al.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/al.png new file mode 100644 index 000000000..59b2b8d3f Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/al.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/am.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/am.png new file mode 100644 index 000000000..750da0651 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/am.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ao.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ao.png new file mode 100644 index 000000000..5161bbedd Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ao.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/aq.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/aq.png new file mode 100644 index 000000000..efa0c2692 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/aq.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ar.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ar.png new file mode 100644 index 000000000..a03bd0560 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ar.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/as.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/as.png new file mode 100644 index 000000000..5eb0dc7ef Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/as.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/at.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/at.png new file mode 100644 index 000000000..541907e23 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/at.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/au.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/au.png new file mode 100644 index 000000000..3a938889f Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/au.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/aw.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/aw.png new file mode 100644 index 000000000..5f9149efe Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/aw.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ax.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ax.png new file mode 100644 index 000000000..9f06c8196 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ax.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/az.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/az.png new file mode 100644 index 000000000..9673ab5a7 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/az.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ba.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ba.png new file mode 100644 index 000000000..6741cf021 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ba.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bb.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bb.png new file mode 100644 index 000000000..bb20cc9fb Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bb.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bd.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bd.png new file mode 100644 index 000000000..705ef7e35 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bd.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/be.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/be.png new file mode 100644 index 000000000..35c2ba83e Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/be.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bf.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bf.png new file mode 100644 index 000000000..fb2f8fff1 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bf.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bg.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bg.png new file mode 100644 index 000000000..afa5aeb25 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bg.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bh.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bh.png new file mode 100644 index 000000000..aba79a4b2 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bh.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bi.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bi.png new file mode 100644 index 000000000..7260b4c39 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bi.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bj.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bj.png new file mode 100644 index 000000000..211a163b6 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bj.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bl.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bl.png new file mode 100644 index 000000000..34c9a8787 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bl.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bm.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bm.png new file mode 100644 index 000000000..1c1c86da3 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bm.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bn.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bn.png new file mode 100644 index 000000000..2e6f20ebb Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bn.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bo.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bo.png new file mode 100644 index 000000000..0143a672c Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bo.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bq.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bq.png new file mode 100644 index 000000000..2be4c18cf Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bq.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/br.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/br.png new file mode 100644 index 000000000..4e338c132 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/br.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bs.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bs.png new file mode 100644 index 000000000..731bfc6fb Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bs.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bt.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bt.png new file mode 100644 index 000000000..07f9f34d0 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bt.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bv.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bv.png new file mode 100644 index 000000000..c9ff046d8 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bv.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bw.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bw.png new file mode 100644 index 000000000..cde0a5f54 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bw.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/by.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/by.png new file mode 100644 index 000000000..4f3ffd885 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/by.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bz.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bz.png new file mode 100644 index 000000000..161cdf1ed Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/bz.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ca.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ca.png new file mode 100644 index 000000000..3790ae517 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ca.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cc.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cc.png new file mode 100644 index 000000000..2dc34b57e Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cc.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cd.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cd.png new file mode 100644 index 000000000..0433ff7e6 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cd.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cf.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cf.png new file mode 100644 index 000000000..3913150b9 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cf.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cg.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cg.png new file mode 100644 index 000000000..8e30e9d7e Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cg.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ch.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ch.png new file mode 100644 index 000000000..7fa87e53d Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ch.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ci.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ci.png new file mode 100644 index 000000000..6a404186f Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ci.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ck.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ck.png new file mode 100644 index 000000000..5cb42801c Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ck.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cl.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cl.png new file mode 100644 index 000000000..11eeb917a Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cl.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cm.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cm.png new file mode 100644 index 000000000..6a2b9b38f Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cm.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cn.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cn.png new file mode 100644 index 000000000..1e39d2ca7 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cn.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/co.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/co.png new file mode 100644 index 000000000..447f8714e Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/co.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cr.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cr.png new file mode 100644 index 000000000..df96cfaaa Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cr.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cu.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cu.png new file mode 100644 index 000000000..160bb489b Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cu.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cv.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cv.png new file mode 100644 index 000000000..b71fb9ea2 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cv.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cw.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cw.png new file mode 100644 index 000000000..2365ec845 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cw.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cx.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cx.png new file mode 100644 index 000000000..44c79e270 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cx.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cy.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cy.png new file mode 100644 index 000000000..1c0c60eca Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cy.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cz.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cz.png new file mode 100644 index 000000000..6280f4d7e Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/cz.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/de.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/de.png new file mode 100644 index 000000000..db3f7db47 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/de.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/dj.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/dj.png new file mode 100644 index 000000000..3ff60b082 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/dj.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/dk.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/dk.png new file mode 100644 index 000000000..6eea46fbc Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/dk.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/dm.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/dm.png new file mode 100644 index 000000000..7fb94f958 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/dm.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/do_flag.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/do_flag.png new file mode 100644 index 000000000..dfba39205 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/do_flag.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/dz.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/dz.png new file mode 100644 index 000000000..85e5bf86b Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/dz.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ec.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ec.png new file mode 100644 index 000000000..0ae8eecac Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ec.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ee.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ee.png new file mode 100644 index 000000000..2a4b9ae04 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ee.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/eg.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/eg.png new file mode 100644 index 000000000..c3a6e9802 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/eg.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/eh.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/eh.png new file mode 100644 index 000000000..22ba31363 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/eh.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/er.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/er.png new file mode 100644 index 000000000..d1bba3ae7 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/er.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/es.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/es.png new file mode 100644 index 000000000..338a34700 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/es.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/et.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/et.png new file mode 100644 index 000000000..e5861dac0 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/et.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/fi.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/fi.png new file mode 100644 index 000000000..a8dacd0ab Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/fi.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/fj.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/fj.png new file mode 100644 index 000000000..c144450d4 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/fj.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/fk.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/fk.png new file mode 100644 index 000000000..9c75d7abe Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/fk.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/fm.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/fm.png new file mode 100644 index 000000000..7731a55da Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/fm.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/fo.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/fo.png new file mode 100644 index 000000000..9c05b1e26 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/fo.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/fr.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/fr.png new file mode 100644 index 000000000..0e3d16c3f Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/fr.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ga.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ga.png new file mode 100644 index 000000000..999588ad3 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ga.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gb.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gb.png new file mode 100644 index 000000000..ffebad314 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gb.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gd.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gd.png new file mode 100644 index 000000000..7f8934b3a Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gd.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ge.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ge.png new file mode 100644 index 000000000..d2dd15ec1 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ge.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gf.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gf.png new file mode 100644 index 000000000..c5d0b2504 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gf.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gg.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gg.png new file mode 100644 index 000000000..8f10041e3 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gg.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gi.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gi.png new file mode 100644 index 000000000..0f5985b9b Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gi.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gl.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gl.png new file mode 100644 index 000000000..acfe5fd51 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gl.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gm.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gm.png new file mode 100644 index 000000000..154552a59 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gm.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gn.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gn.png new file mode 100644 index 000000000..ecc7aed83 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gn.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gp.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gp.png new file mode 100644 index 000000000..75d7f46e8 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gp.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gq.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gq.png new file mode 100644 index 000000000..f7bf7ac3c Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gq.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gr.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gr.png new file mode 100644 index 000000000..be17af821 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gr.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gs.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gs.png new file mode 100644 index 000000000..73ebc9f07 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gs.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gt.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gt.png new file mode 100644 index 000000000..37c400608 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gt.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gu.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gu.png new file mode 100644 index 000000000..cdb2617c5 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gu.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gw.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gw.png new file mode 100644 index 000000000..387962a6b Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gw.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gy.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gy.png new file mode 100644 index 000000000..46c56f1d2 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/gy.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/hk.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/hk.png new file mode 100644 index 000000000..f3e1bdfec Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/hk.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/hm.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/hm.png new file mode 100644 index 000000000..6bdd23868 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/hm.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/hn.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/hn.png new file mode 100644 index 000000000..5778bc45f Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/hn.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/hr.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/hr.png new file mode 100644 index 000000000..802613371 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/hr.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ht.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ht.png new file mode 100644 index 000000000..7a0601c50 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ht.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/hu.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/hu.png new file mode 100644 index 000000000..d87ba073a Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/hu.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/id.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/id.png new file mode 100644 index 000000000..4e22bccc3 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/id.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ie.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ie.png new file mode 100644 index 000000000..49aef004d Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ie.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/il.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/il.png new file mode 100644 index 000000000..5b033849c Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/il.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/im.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/im.png new file mode 100644 index 000000000..02162a117 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/im.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/in.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/in.png new file mode 100644 index 000000000..03300d8be Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/in.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/io.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/io.png new file mode 100644 index 000000000..dc0ac4f87 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/io.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/iq.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/iq.png new file mode 100644 index 000000000..fbac7587f Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/iq.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ir.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ir.png new file mode 100644 index 000000000..c172ad9d8 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ir.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/is.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/is.png new file mode 100644 index 000000000..aa611d797 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/is.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/it.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/it.png new file mode 100644 index 000000000..353b20e74 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/it.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/je.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/je.png new file mode 100644 index 000000000..eb01f5a0b Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/je.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/jm.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/jm.png new file mode 100644 index 000000000..e80702766 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/jm.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/jo.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/jo.png new file mode 100644 index 000000000..30e353f47 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/jo.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/jp.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/jp.png new file mode 100644 index 000000000..b432601be Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/jp.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ke.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ke.png new file mode 100644 index 000000000..c2f2a4a7a Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ke.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/kg.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/kg.png new file mode 100644 index 000000000..34fb6df58 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/kg.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/kh.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/kh.png new file mode 100644 index 000000000..8ca2eaa1d Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/kh.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ki.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ki.png new file mode 100644 index 000000000..b7a89ba59 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ki.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/km.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/km.png new file mode 100644 index 000000000..0b64f4308 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/km.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/kn.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/kn.png new file mode 100644 index 000000000..338d6de3f Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/kn.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/kp.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/kp.png new file mode 100644 index 000000000..31683b654 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/kp.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/kr.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/kr.png new file mode 100644 index 000000000..86deaad10 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/kr.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/kw.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/kw.png new file mode 100644 index 000000000..0a31fe937 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/kw.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ky.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ky.png new file mode 100644 index 000000000..c4238b3b4 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ky.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/kz.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/kz.png new file mode 100644 index 000000000..0d1377e48 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/kz.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/la.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/la.png new file mode 100644 index 000000000..d4d078417 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/la.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/lb.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/lb.png new file mode 100644 index 000000000..4d81bdef6 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/lb.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/lc.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/lc.png new file mode 100644 index 000000000..971bc3773 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/lc.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/li.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/li.png new file mode 100644 index 000000000..26047468c Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/li.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/lk.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/lk.png new file mode 100644 index 000000000..08f7d6a7e Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/lk.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/lr.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/lr.png new file mode 100644 index 000000000..ef4462988 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/lr.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ls.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ls.png new file mode 100644 index 000000000..4d7721d3c Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ls.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/lt.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/lt.png new file mode 100644 index 000000000..8d861b7ed Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/lt.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/lu.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/lu.png new file mode 100644 index 000000000..2b28a35d3 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/lu.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/lv.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/lv.png new file mode 100644 index 000000000..a6ef25feb Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/lv.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ly.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ly.png new file mode 100644 index 000000000..2fedcc1dd Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ly.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ma.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ma.png new file mode 100644 index 000000000..f89c2e001 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ma.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mc.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mc.png new file mode 100644 index 000000000..be057f99b Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mc.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/md.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/md.png new file mode 100644 index 000000000..65fbb0b1a Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/md.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/me.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/me.png new file mode 100644 index 000000000..6a80a7525 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/me.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mf.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mf.png new file mode 100644 index 000000000..332a8e3fc Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mf.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mg.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mg.png new file mode 100644 index 000000000..9eb2cf69c Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mg.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mh.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mh.png new file mode 100644 index 000000000..1d17d4d1b Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mh.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mk.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mk.png new file mode 100644 index 000000000..da1b9abcb Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mk.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ml.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ml.png new file mode 100644 index 000000000..a87c0abdc Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ml.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mm.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mm.png new file mode 100644 index 000000000..d9f9032c8 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mm.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mn.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mn.png new file mode 100644 index 000000000..e8f9d9d1a Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mn.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mo.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mo.png new file mode 100644 index 000000000..f7ba1c3c1 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mo.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mp.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mp.png new file mode 100644 index 000000000..58c74fc67 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mp.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mq.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mq.png new file mode 100644 index 000000000..b723c009e Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mq.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mr.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mr.png new file mode 100644 index 000000000..285f73740 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mr.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ms.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ms.png new file mode 100644 index 000000000..94d50aa75 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ms.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mt.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mt.png new file mode 100644 index 000000000..f2b0ce0b6 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mt.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mu.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mu.png new file mode 100644 index 000000000..755dce208 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mu.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mv.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mv.png new file mode 100644 index 000000000..31eac72d2 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mv.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mw.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mw.png new file mode 100644 index 000000000..d649d1144 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mw.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mx.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mx.png new file mode 100644 index 000000000..fdb43b6de Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mx.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/my.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/my.png new file mode 100644 index 000000000..db64b56c5 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/my.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mz.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mz.png new file mode 100644 index 000000000..1d6521978 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/mz.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/na.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/na.png new file mode 100644 index 000000000..4c696bca1 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/na.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/nc.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/nc.png new file mode 100644 index 000000000..3b188515d Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/nc.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ne.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ne.png new file mode 100644 index 000000000..959afa7fd Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ne.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/nf.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/nf.png new file mode 100644 index 000000000..88c7fcc4e Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/nf.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ng.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ng.png new file mode 100644 index 000000000..4d59d5440 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ng.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ni.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ni.png new file mode 100644 index 000000000..6a938c94e Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ni.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/nl.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/nl.png new file mode 100644 index 000000000..b3d928a85 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/nl.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/no.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/no.png new file mode 100644 index 000000000..326de62d1 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/no.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/np.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/np.png new file mode 100644 index 000000000..c9916676d Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/np.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/nr.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/nr.png new file mode 100644 index 000000000..95223d3ba Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/nr.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/nu.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/nu.png new file mode 100644 index 000000000..082d19410 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/nu.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/nz.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/nz.png new file mode 100644 index 000000000..2ef9bab7d Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/nz.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/om.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/om.png new file mode 100644 index 000000000..79939f3c6 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/om.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/pa.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/pa.png new file mode 100644 index 000000000..fbafa66e2 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/pa.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/pe.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/pe.png new file mode 100644 index 000000000..744008621 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/pe.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/pf.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/pf.png new file mode 100644 index 000000000..000c41d14 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/pf.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/pg.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/pg.png new file mode 100644 index 000000000..998c227ad Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/pg.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ph.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ph.png new file mode 100644 index 000000000..a604cdcab Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ph.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/pk.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/pk.png new file mode 100644 index 000000000..6637d24d6 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/pk.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/pl.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/pl.png new file mode 100644 index 000000000..ec7a4954b Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/pl.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/pm.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/pm.png new file mode 100644 index 000000000..7af069182 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/pm.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/pn.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/pn.png new file mode 100644 index 000000000..7c344ea73 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/pn.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/pr.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/pr.png new file mode 100644 index 000000000..7da5cd418 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/pr.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ps.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ps.png new file mode 100644 index 000000000..7a26daea8 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ps.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/pt.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/pt.png new file mode 100644 index 000000000..38d60e9c1 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/pt.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/pw.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/pw.png new file mode 100644 index 000000000..cec7edebe Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/pw.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/py.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/py.png new file mode 100644 index 000000000..84a78cf4b Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/py.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/qa.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/qa.png new file mode 100644 index 000000000..3dd855689 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/qa.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/re.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/re.png new file mode 100644 index 000000000..ff4cd7cb3 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/re.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ro.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ro.png new file mode 100644 index 000000000..70b8505fe Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ro.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/rs.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/rs.png new file mode 100644 index 000000000..73c053139 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/rs.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ru.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ru.png new file mode 100644 index 000000000..9e86166a3 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ru.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/rw.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/rw.png new file mode 100644 index 000000000..ff0476fa8 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/rw.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sa.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sa.png new file mode 100644 index 000000000..190963fd5 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sa.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sb.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sb.png new file mode 100644 index 000000000..e92ecffe8 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sb.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sc.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sc.png new file mode 100644 index 000000000..519821fd8 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sc.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sd.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sd.png new file mode 100644 index 000000000..6fea21812 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sd.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/se.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/se.png new file mode 100644 index 000000000..0bc7c93ce Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/se.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sg.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sg.png new file mode 100644 index 000000000..1e3e8a0d6 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sg.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sh.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sh.png new file mode 100644 index 000000000..670eb8eab Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sh.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/si.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/si.png new file mode 100644 index 000000000..0f2cd6f02 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/si.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sj.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sj.png new file mode 100644 index 000000000..51905cc11 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sj.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sk.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sk.png new file mode 100644 index 000000000..8d860bced Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sk.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sl.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sl.png new file mode 100644 index 000000000..7d898bdbd Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sl.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sm.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sm.png new file mode 100644 index 000000000..06a6bda12 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sm.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sn.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sn.png new file mode 100644 index 000000000..8b813b3a0 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sn.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/so.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/so.png new file mode 100644 index 000000000..ba9ce3eeb Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/so.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sr.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sr.png new file mode 100644 index 000000000..9e2d1caed Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sr.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ss.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ss.png new file mode 100644 index 000000000..582710c29 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ss.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/st.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/st.png new file mode 100644 index 000000000..0fa430c93 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/st.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sv.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sv.png new file mode 100644 index 000000000..d6f48971d Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sv.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sx.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sx.png new file mode 100644 index 000000000..f6b855871 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sx.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sy.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sy.png new file mode 100644 index 000000000..25ce12f15 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sy.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sz.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sz.png new file mode 100644 index 000000000..7f27ab730 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/sz.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tc.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tc.png new file mode 100644 index 000000000..e334188fa Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tc.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/td.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/td.png new file mode 100644 index 000000000..991f9b1e0 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/td.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tf.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tf.png new file mode 100644 index 000000000..88df1bf7b Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tf.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tg.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tg.png new file mode 100644 index 000000000..fc7a3a920 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tg.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/th.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/th.png new file mode 100644 index 000000000..03fd23b0e Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/th.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tj.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tj.png new file mode 100644 index 000000000..5c3b062de Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tj.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tk.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tk.png new file mode 100644 index 000000000..d6f69805e Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tk.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tl.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tl.png new file mode 100644 index 000000000..8e39f3810 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tl.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tm.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tm.png new file mode 100644 index 000000000..bf7186fd1 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tm.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tn.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tn.png new file mode 100644 index 000000000..5b83512b6 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tn.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/to.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/to.png new file mode 100644 index 000000000..3b861fc9c Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/to.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tr.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tr.png new file mode 100644 index 000000000..31f5edff1 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tr.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tt.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tt.png new file mode 100644 index 000000000..fdff2b2ce Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tt.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tv.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tv.png new file mode 100644 index 000000000..131956f6d Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tv.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tw.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tw.png new file mode 100644 index 000000000..9dc8fec72 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tw.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tz.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tz.png new file mode 100644 index 000000000..73180f000 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/tz.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ua.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ua.png new file mode 100644 index 000000000..558ec3fbf Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ua.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ug.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ug.png new file mode 100644 index 000000000..6003d6f26 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ug.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/um.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/um.png new file mode 100644 index 000000000..e092541af Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/um.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/us.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/us.png new file mode 100644 index 000000000..e092541af Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/us.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/uy.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/uy.png new file mode 100644 index 000000000..302f6d9ad Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/uy.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/uz.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/uz.png new file mode 100644 index 000000000..7eb9d0541 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/uz.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/va.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/va.png new file mode 100644 index 000000000..48eae4583 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/va.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/vc.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/vc.png new file mode 100644 index 000000000..c98be731d Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/vc.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ve.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ve.png new file mode 100644 index 000000000..77e9416de Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ve.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/vg.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/vg.png new file mode 100644 index 000000000..7f87c68ea Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/vg.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/vi.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/vi.png new file mode 100644 index 000000000..e7eb8f7fb Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/vi.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/vn.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/vn.png new file mode 100644 index 000000000..a290df621 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/vn.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/vu.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/vu.png new file mode 100644 index 000000000..a0d648ffa Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/vu.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/wf.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/wf.png new file mode 100644 index 000000000..06e55d2c1 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/wf.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ws.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ws.png new file mode 100644 index 000000000..04adf0673 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ws.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/xk.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/xk.png new file mode 100644 index 000000000..84834502b Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/xk.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ye.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ye.png new file mode 100644 index 000000000..5325964fe Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/ye.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/yt.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/yt.png new file mode 100644 index 000000000..19a0e4a5b Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/yt.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/za.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/za.png new file mode 100644 index 000000000..e329e2e18 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/za.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/zm.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/zm.png new file mode 100644 index 000000000..422ba6436 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/zm.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/zw.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/zw.png new file mode 100644 index 000000000..9cf6b5eaa Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/zw.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/zz.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/zz.png new file mode 100644 index 000000000..fdedbb42e Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxhdpi/zz.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxxhdpi/ic_alert.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxxhdpi/ic_alert.png new file mode 100644 index 000000000..5ab36c472 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxxhdpi/ic_alert.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxxhdpi/ic_error.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxxhdpi/ic_error.png new file mode 100644 index 000000000..fd994a23e Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxxhdpi/ic_error.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxxhdpi/ic_filled.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxxhdpi/ic_filled.png new file mode 100644 index 000000000..b7bcc6b59 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxxhdpi/ic_filled.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxxhdpi/ic_lines.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxxhdpi/ic_lines.png new file mode 100644 index 000000000..2b8a9282a Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxxhdpi/ic_lines.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxxhdpi/modal_background.9.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxxhdpi/modal_background.9.png new file mode 100644 index 000000000..f7a2eb9ef Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable-xxxhdpi/modal_background.9.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable/background.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable/background.png new file mode 100644 index 000000000..b4144fdf7 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable/background.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable/box_background_pattern_tiling.xml b/cmd/skywirevisormobile/android/app/src/main/res/drawable/box_background_pattern_tiling.xml new file mode 100644 index 000000000..4708111a5 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/res/drawable/box_background_pattern_tiling.xml @@ -0,0 +1,4 @@ + diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable/box_clip_area_left.xml b/cmd/skywirevisormobile/android/app/src/main/res/drawable/box_clip_area_left.xml new file mode 100644 index 000000000..e9d2a8c10 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/res/drawable/box_clip_area_left.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable/box_clip_area_right.xml b/cmd/skywirevisormobile/android/app/src/main/res/drawable/box_clip_area_right.xml new file mode 100644 index 000000000..ff8e69ee6 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/res/drawable/box_clip_area_right.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable/box_ripple.xml b/cmd/skywirevisormobile/android/app/src/main/res/drawable/box_ripple.xml new file mode 100644 index 000000000..a5a4603ff --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/res/drawable/box_ripple.xml @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable/box_row_rounded_box_1.xml b/cmd/skywirevisormobile/android/app/src/main/res/drawable/box_row_rounded_box_1.xml new file mode 100644 index 000000000..cd88329f3 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/res/drawable/box_row_rounded_box_1.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable/box_row_rounded_box_2.xml b/cmd/skywirevisormobile/android/app/src/main/res/drawable/box_row_rounded_box_2.xml new file mode 100644 index 000000000..31d96f34f --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/res/drawable/box_row_rounded_box_2.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable/box_row_rounded_box_3.xml b/cmd/skywirevisormobile/android/app/src/main/res/drawable/box_row_rounded_box_3.xml new file mode 100644 index 000000000..1f2be538e --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/res/drawable/box_row_rounded_box_3.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable/box_row_rounded_box_4.xml b/cmd/skywirevisormobile/android/app/src/main/res/drawable/box_row_rounded_box_4.xml new file mode 100644 index 000000000..3df744bab --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/res/drawable/box_row_rounded_box_4.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable/box_row_rounded_box_5.xml b/cmd/skywirevisormobile/android/app/src/main/res/drawable/box_row_rounded_box_5.xml new file mode 100644 index 000000000..5ef7f1056 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/res/drawable/box_row_rounded_box_5.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable/clear_box_ripple.xml b/cmd/skywirevisormobile/android/app/src/main/res/drawable/clear_box_ripple.xml new file mode 100644 index 000000000..cdac0f21d --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/res/drawable/clear_box_ripple.xml @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable/current_server_ripple.xml b/cmd/skywirevisormobile/android/app/src/main/res/drawable/current_server_ripple.xml new file mode 100644 index 000000000..85b80d9bf --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/res/drawable/current_server_ripple.xml @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable/current_server_rounded_box.xml b/cmd/skywirevisormobile/android/app/src/main/res/drawable/current_server_rounded_box.xml new file mode 100644 index 000000000..2ba99e7d1 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/res/drawable/current_server_rounded_box.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable/flag_rounded_box.xml b/cmd/skywirevisormobile/android/app/src/main/res/drawable/flag_rounded_box.xml new file mode 100644 index 000000000..4c6bbe75e --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/res/drawable/flag_rounded_box.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable/internal_box_row_rounded_box_1.xml b/cmd/skywirevisormobile/android/app/src/main/res/drawable/internal_box_row_rounded_box_1.xml new file mode 100644 index 000000000..71083003f --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/res/drawable/internal_box_row_rounded_box_1.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable/internal_box_row_rounded_box_2.xml b/cmd/skywirevisormobile/android/app/src/main/res/drawable/internal_box_row_rounded_box_2.xml new file mode 100644 index 000000000..26018e3aa --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/res/drawable/internal_box_row_rounded_box_2.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable/internal_box_row_rounded_box_3.xml b/cmd/skywirevisormobile/android/app/src/main/res/drawable/internal_box_row_rounded_box_3.xml new file mode 100644 index 000000000..e86d1fe08 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/res/drawable/internal_box_row_rounded_box_3.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable/internal_box_row_rounded_box_4.xml b/cmd/skywirevisormobile/android/app/src/main/res/drawable/internal_box_row_rounded_box_4.xml new file mode 100644 index 000000000..5ef7f1056 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/res/drawable/internal_box_row_rounded_box_4.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable/map.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable/map.png new file mode 100644 index 000000000..1218ddbc9 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable/map.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable/modal_background_pattern_tiling.xml b/cmd/skywirevisormobile/android/app/src/main/res/drawable/modal_background_pattern_tiling.xml new file mode 100644 index 000000000..e31574bdf --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/res/drawable/modal_background_pattern_tiling.xml @@ -0,0 +1,4 @@ + diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable/modal_button_primary_background.xml b/cmd/skywirevisormobile/android/app/src/main/res/drawable/modal_button_primary_background.xml new file mode 100644 index 000000000..14d5e6874 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/res/drawable/modal_button_primary_background.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable/modal_button_primary_ripple.xml b/cmd/skywirevisormobile/android/app/src/main/res/drawable/modal_button_primary_ripple.xml new file mode 100644 index 000000000..142ef1097 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/res/drawable/modal_button_primary_ripple.xml @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable/modal_button_secondary_background.xml b/cmd/skywirevisormobile/android/app/src/main/res/drawable/modal_button_secondary_background.xml new file mode 100644 index 000000000..2973e01b9 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/res/drawable/modal_button_secondary_background.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable/modal_button_secondary_ripple.xml b/cmd/skywirevisormobile/android/app/src/main/res/drawable/modal_button_secondary_ripple.xml new file mode 100644 index 000000000..0eb4edfc3 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/res/drawable/modal_button_secondary_ripple.xml @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable/modal_internal_area.xml b/cmd/skywirevisormobile/android/app/src/main/res/drawable/modal_internal_area.xml new file mode 100644 index 000000000..4f9212b84 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/res/drawable/modal_internal_area.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable/red_button_pattern_tiling.xml b/cmd/skywirevisormobile/android/app/src/main/res/drawable/red_button_pattern_tiling.xml new file mode 100644 index 000000000..4708111a5 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/res/drawable/red_button_pattern_tiling.xml @@ -0,0 +1,4 @@ + diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable/stop_btn_internal_area.xml b/cmd/skywirevisormobile/android/app/src/main/res/drawable/stop_btn_internal_area.xml new file mode 100644 index 000000000..571fe729c --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/res/drawable/stop_btn_internal_area.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable/tablet_tab_border.xml b/cmd/skywirevisormobile/android/app/src/main/res/drawable/tablet_tab_border.xml new file mode 100644 index 000000000..2574f6f64 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/res/drawable/tablet_tab_border.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable/time_rounded_box.xml b/cmd/skywirevisormobile/android/app/src/main/res/drawable/time_rounded_box.xml new file mode 100644 index 000000000..3f4b67587 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/res/drawable/time_rounded_box.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/cmd/skywirevisormobile/android/app/src/main/res/drawable/top_bar_shadow.png b/cmd/skywirevisormobile/android/app/src/main/res/drawable/top_bar_shadow.png new file mode 100644 index 000000000..5706222b3 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/drawable/top_bar_shadow.png differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/font/material_font.ttf b/cmd/skywirevisormobile/android/app/src/main/res/font/material_font.ttf new file mode 100644 index 000000000..e50801b3b Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/font/material_font.ttf differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/font/skycoin_font.otf b/cmd/skywirevisormobile/android/app/src/main/res/font/skycoin_font.otf new file mode 100644 index 000000000..e3e80f0e4 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/font/skycoin_font.otf differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/font/skycoin_font_bold.otf b/cmd/skywirevisormobile/android/app/src/main/res/font/skycoin_font_bold.otf new file mode 100644 index 000000000..9ef702074 Binary files /dev/null and b/cmd/skywirevisormobile/android/app/src/main/res/font/skycoin_font_bold.otf differ diff --git a/cmd/skywirevisormobile/android/app/src/main/res/layout/activity_app_list.xml b/cmd/skywirevisormobile/android/app/src/main/res/layout/activity_app_list.xml new file mode 100644 index 000000000..7c02a596c --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/res/layout/activity_app_list.xml @@ -0,0 +1,28 @@ + + + + + + + + + diff --git a/cmd/skywirevisormobile/android/app/src/main/res/layout/activity_index.xml b/cmd/skywirevisormobile/android/app/src/main/res/layout/activity_index.xml new file mode 100644 index 000000000..a909194a8 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/res/layout/activity_index.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + diff --git a/cmd/skywirevisormobile/android/app/src/main/res/layout/activity_main.xml b/cmd/skywirevisormobile/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000..e69786e05 --- /dev/null +++ b/cmd/skywirevisormobile/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,125 @@ + + + + + + + + + + + \n );\n}\n","import * as React from 'react';\nimport cx from 'clsx';\n\nimport { TYPE, Default, isFn } from './../utils';\nimport { TypeOptions, ToastClassName, Theme } from '../types';\n\nexport interface ProgressBarProps {\n /**\n * The animation delay which determine when to close the toast\n */\n delay: number;\n\n /**\n * Whether or not the animation is running or paused\n */\n isRunning: boolean;\n\n /**\n * Func to close the current toast\n */\n closeToast: () => void;\n\n /**\n * Optional type : info, success ...\n */\n type: TypeOptions;\n\n /**\n * The theme that is currently used\n */\n theme: Theme;\n\n /**\n * Hide or not the progress bar\n */\n hide?: boolean;\n\n /**\n * Optionnal className\n */\n className?: ToastClassName;\n\n /**\n * Optionnal inline style\n */\n style?: React.CSSProperties;\n\n /**\n * Tell wether or not controlled progress bar is used\n */\n controlledProgress?: boolean;\n\n /**\n * Controlled progress value\n */\n progress?: number | string;\n\n /**\n * Support rtl content\n */\n rtl?: boolean;\n\n /**\n * Tell if the component is visible on screen or not\n */\n isIn?: boolean;\n}\n\nexport function ProgressBar({\n delay,\n isRunning,\n closeToast,\n type,\n hide,\n className,\n style: userStyle,\n controlledProgress,\n progress,\n rtl,\n isIn,\n theme\n}: ProgressBarProps) {\n const style: React.CSSProperties = {\n ...userStyle,\n animationDuration: `${delay}ms`,\n animationPlayState: isRunning ? 'running' : 'paused',\n opacity: hide ? 0 : 1\n };\n\n if (controlledProgress) style.transform = `scaleX(${progress})`;\n const defaultClassName = cx(\n `${Default.CSS_NAMESPACE}__progress-bar`,\n controlledProgress\n ? `${Default.CSS_NAMESPACE}__progress-bar--controlled`\n : `${Default.CSS_NAMESPACE}__progress-bar--animated`,\n `${Default.CSS_NAMESPACE}__progress-bar-theme--${theme}`,\n `${Default.CSS_NAMESPACE}__progress-bar--${type}`,\n {\n [`${Default.CSS_NAMESPACE}__progress-bar--rtl`]: rtl\n }\n );\n const classNames = isFn(className)\n ? className({\n rtl,\n type,\n defaultClassName\n })\n : cx(defaultClassName, className);\n\n // 🧐 controlledProgress is derived from progress\n // so if controlledProgress is set\n // it means that this is also the case for progress\n const animationEvent = {\n [controlledProgress && progress! >= 1\n ? 'onTransitionEnd'\n : 'onAnimationEnd']:\n controlledProgress && progress! < 1\n ? null\n : () => {\n isIn && closeToast();\n }\n };\n\n // TODO: add aria-valuenow, aria-valuemax, aria-valuemin\n\n return (\n \n );\n}\n\nProgressBar.defaultProps = {\n type: TYPE.DEFAULT,\n hide: false\n};\n","import React from 'react';\n\nimport { Theme, TypeOptions } from '../types';\nimport { Default } from '../utils';\n\n/**\n * Used when providing custom icon\n */\nexport interface IconProps {\n theme: Theme;\n type: TypeOptions;\n}\n\nexport type BuiltInIconProps = React.SVGProps & IconProps;\n\nconst Svg: React.FC = ({ theme, type, ...rest }) => (\n \n);\n\nfunction Warning(props: BuiltInIconProps) {\n return (\n \n \n \n );\n}\n\nfunction Info(props: BuiltInIconProps) {\n return (\n \n \n \n );\n}\n\nfunction Success(props: BuiltInIconProps) {\n return (\n \n \n \n );\n}\n\nfunction Error(props: BuiltInIconProps) {\n return (\n \n \n \n );\n}\n\nfunction Spinner() {\n return
;\n}\n\nexport const Icons = {\n info: Info,\n warning: Warning,\n success: Success,\n error: Error,\n spinner: Spinner\n};\n","import * as React from 'react';\nimport cx from 'clsx';\n\nimport { ProgressBar } from './ProgressBar';\nimport { Icons } from './Icons';\nimport { ToastProps } from '../types';\nimport { Default, isFn, isStr } from '../utils';\nimport { useToast } from '../hooks';\n\nexport const Toast: React.FC = props => {\n const {\n isRunning,\n preventExitTransition,\n toastRef,\n eventHandlers\n } = useToast(props);\n const {\n closeButton,\n children,\n autoClose,\n onClick,\n type,\n hideProgressBar,\n closeToast,\n transition: Transition,\n position,\n className,\n style,\n bodyClassName,\n bodyStyle,\n progressClassName,\n progressStyle,\n updateId,\n role,\n progress,\n rtl,\n toastId,\n deleteToast,\n isIn,\n isLoading,\n icon,\n theme\n } = props;\n const defaultClassName = cx(\n `${Default.CSS_NAMESPACE}__toast`,\n `${Default.CSS_NAMESPACE}__toast-theme--${theme}`,\n `${Default.CSS_NAMESPACE}__toast--${type}`,\n {\n [`${Default.CSS_NAMESPACE}__toast--rtl`]: rtl\n }\n );\n const cssClasses = isFn(className)\n ? className({\n rtl,\n position,\n type,\n defaultClassName\n })\n : cx(defaultClassName, className);\n const isProgressControlled = !!progress;\n const maybeIcon = Icons[type as keyof typeof Icons];\n const iconProps = { theme, type };\n let Icon: React.ReactNode = maybeIcon && maybeIcon(iconProps);\n\n if (icon === false) {\n Icon = void 0;\n } else if (isFn(icon)) {\n Icon = icon(iconProps);\n } else if (React.isValidElement(icon)) {\n Icon = React.cloneElement(icon, iconProps);\n } else if (isStr(icon)) {\n Icon = icon;\n } else if (isLoading) {\n Icon = Icons.spinner();\n }\n\n function renderCloseButton(closeButton: any) {\n if (!closeButton) return;\n\n const props = { closeToast, type, theme };\n\n if (isFn(closeButton)) return closeButton(props);\n\n if (React.isValidElement(closeButton))\n return React.cloneElement(closeButton, props);\n }\n\n return (\n \n \n \n {Icon && (\n \n {Icon}\n
\n )}\n
{children}
\n \n {renderCloseButton(closeButton)}\n {(autoClose || isProgressControlled) && (\n \n )}\n \n \n );\n};\n","import { Default, cssTransition } from '../utils';\n\nconst Bounce = cssTransition({\n enter: `${Default.CSS_NAMESPACE}--animate ${Default.CSS_NAMESPACE}__bounce-enter`,\n exit: `${Default.CSS_NAMESPACE}--animate ${Default.CSS_NAMESPACE}__bounce-exit`,\n appendPosition: true\n});\n\nconst Slide = cssTransition({\n enter: `${Default.CSS_NAMESPACE}--animate ${Default.CSS_NAMESPACE}__slide-enter`,\n exit: `${Default.CSS_NAMESPACE}--animate ${Default.CSS_NAMESPACE}__slide-exit`,\n appendPosition: true\n});\n\nconst Zoom = cssTransition({\n enter: `${Default.CSS_NAMESPACE}--animate ${Default.CSS_NAMESPACE}__zoom-enter`,\n exit: `${Default.CSS_NAMESPACE}--animate ${Default.CSS_NAMESPACE}__zoom-exit`\n});\n\nconst Flip = cssTransition({\n enter: `${Default.CSS_NAMESPACE}--animate ${Default.CSS_NAMESPACE}__flip-enter`,\n exit: `${Default.CSS_NAMESPACE}--animate ${Default.CSS_NAMESPACE}__flip-exit`\n});\n\nexport { Bounce, Slide, Zoom, Flip };\n","import * as React from 'react';\nimport cx from 'clsx';\n\nimport { Toast } from './Toast';\nimport { CloseButton } from './CloseButton';\nimport { Bounce } from './Transitions';\nimport { POSITION, Direction, Default, parseClassName, isFn } from '../utils';\nimport { useToastContainer } from '../hooks';\nimport { ToastContainerProps, ToastPosition } from '../types';\n\nexport const ToastContainer: React.FC = props => {\n const { getToastToRender, containerRef, isToastActive } = useToastContainer(\n props\n );\n const { className, style, rtl, containerId } = props;\n\n function getClassName(position: ToastPosition) {\n const defaultClassName = cx(\n `${Default.CSS_NAMESPACE}__toast-container`,\n `${Default.CSS_NAMESPACE}__toast-container--${position}`,\n { [`${Default.CSS_NAMESPACE}__toast-container--rtl`]: rtl }\n );\n return isFn(className)\n ? className({\n position,\n rtl,\n defaultClassName\n })\n : cx(defaultClassName, parseClassName(className));\n }\n\n return (\n \n {getToastToRender((position, toastList) => {\n const containerStyle: React.CSSProperties =\n toastList.length === 0\n ? { ...style, pointerEvents: 'none' }\n : { ...style };\n\n return (\n \n {toastList.map(({ content, props: toastProps }) => {\n return (\n \n {content}\n \n );\n })}\n \n );\n })}\n \n );\n};\n\nToastContainer.defaultProps = {\n position: POSITION.TOP_RIGHT as ToastPosition,\n transition: Bounce,\n rtl: false,\n autoClose: 5000,\n hideProgressBar: false,\n closeButton: CloseButton,\n pauseOnHover: true,\n pauseOnFocusLoss: true,\n closeOnClick: true,\n newestOnTop: false,\n draggable: true,\n draggablePercent: Default.DRAGGABLE_PERCENT as number,\n draggableDirection: Direction.X,\n role: 'alert',\n theme: 'light'\n};\n","import * as React from 'react';\nimport { render } from 'react-dom';\n\nimport { POSITION, TYPE, canUseDom, isStr, isNum, isFn } from '../utils';\nimport { eventManager, OnChangeCallback, Event } from './eventManager';\nimport {\n ToastContent,\n ToastOptions,\n ToastProps,\n Id,\n ToastContainerProps,\n UpdateOptions,\n ClearWaitingQueueParams,\n NotValidatedToastProps,\n TypeOptions\n} from '../types';\nimport { ContainerInstance } from '../hooks';\nimport { ToastContainer } from '../components';\n\ninterface EnqueuedToast {\n content: ToastContent;\n options: NotValidatedToastProps;\n}\n\nlet containers = new Map();\nlet latestInstance: ContainerInstance | Id;\nlet containerDomNode: HTMLElement;\nlet containerConfig: ToastContainerProps;\nlet queue: EnqueuedToast[] = [];\nlet lazy = false;\n\n/**\n * Check whether any container is currently mounted in the DOM\n */\nfunction isAnyContainerMounted() {\n return containers.size > 0;\n}\n\n/**\n * Get the toast by id, given it's in the DOM, otherwise returns null\n */\nfunction getToast(toastId: Id, { containerId }: ToastOptions) {\n const container = containers.get(containerId || latestInstance);\n if (!container) return null;\n\n return container.getToast(toastId);\n}\n\n/**\n * Generate a random toastId\n */\nfunction generateToastId() {\n return Math.random()\n .toString(36)\n .substr(2, 9);\n}\n\n/**\n * Generate a toastId or use the one provided\n */\nfunction getToastId(options?: ToastOptions) {\n if (options && (isStr(options.toastId) || isNum(options.toastId))) {\n return options.toastId;\n }\n\n return generateToastId();\n}\n\n/**\n * If the container is not mounted, the toast is enqueued and\n * the container lazy mounted\n */\nfunction dispatchToast(\n content: ToastContent,\n options: NotValidatedToastProps\n): Id {\n if (isAnyContainerMounted()) {\n eventManager.emit(Event.Show, content, options);\n } else {\n queue.push({ content, options });\n if (lazy && canUseDom) {\n lazy = false;\n containerDomNode = document.createElement('div');\n document.body.appendChild(containerDomNode);\n render(, containerDomNode);\n }\n }\n\n return options.toastId;\n}\n\n/**\n * Merge provided options with the defaults settings and generate the toastId\n */\nfunction mergeOptions(type: string, options?: ToastOptions) {\n return {\n ...options,\n type: (options && options.type) || type,\n toastId: getToastId(options)\n } as NotValidatedToastProps;\n}\n\nconst createToastByType = (type: string) => (\n content: ToastContent,\n options?: ToastOptions\n) => dispatchToast(content, mergeOptions(type, options));\n\nconst toast = (content: ToastContent, options?: ToastOptions) =>\n dispatchToast(content, mergeOptions(TYPE.DEFAULT, options));\n\ntoast.loading = (content: ToastContent, options?: ToastOptions) =>\n dispatchToast(\n content,\n mergeOptions(TYPE.DEFAULT, {\n isLoading: true,\n autoClose: false,\n closeOnClick: false,\n closeButton: false,\n draggable: false,\n ...options\n })\n );\n\ninterface ToastPromiseParams {\n pending?: string | UpdateOptions;\n success?: string | UpdateOptions;\n error?: string | UpdateOptions;\n}\n\nfunction handlePromise(\n promise: Promise | (() => Promise),\n { pending, error, success }: ToastPromiseParams,\n options?: ToastOptions\n) {\n let id: Id;\n\n if (pending) {\n id = isStr(pending)\n ? toast.loading(pending, options)\n : toast.loading(pending.render, {\n ...options,\n ...(pending as ToastOptions)\n });\n }\n\n const resetParams = {\n isLoading: null,\n autoClose: null,\n closeOnClick: null,\n closeButton: null,\n draggable: null\n };\n\n const resolver = (\n type: TypeOptions,\n input: string | UpdateOptions,\n result: T\n ) => {\n const baseParams = {\n type,\n ...resetParams,\n ...options,\n data: result\n };\n const params = isStr(input) ? { render: input } : input;\n\n // if the id is set we know that it's an update\n if (id) {\n toast.update(id, {\n ...baseParams,\n ...params\n });\n } else {\n // using toast.promise without loading\n toast(params.render, {\n ...baseParams,\n ...params\n } as ToastOptions);\n }\n\n return result;\n };\n\n const p = isFn(promise) ? promise() : promise;\n\n //call the resolvers only when needed\n p.then(result => success && resolver('success', success, result)).catch(\n err => error && resolver('error', error, err)\n );\n\n return p;\n}\n\ntoast.promise = handlePromise;\ntoast.success = createToastByType(TYPE.SUCCESS);\ntoast.info = createToastByType(TYPE.INFO);\ntoast.error = createToastByType(TYPE.ERROR);\ntoast.warning = createToastByType(TYPE.WARNING);\ntoast.warn = toast.warning;\ntoast.dark = (content: ToastContent, options?: ToastOptions) =>\n dispatchToast(\n content,\n mergeOptions(TYPE.DEFAULT, {\n theme: 'dark',\n ...options\n })\n );\n\n/**\n * Remove toast programmaticaly\n */\ntoast.dismiss = (id?: Id) => eventManager.emit(Event.Clear, id);\n\n/**\n * Clear waiting queue when limit is used\n */\ntoast.clearWaitingQueue = (params: ClearWaitingQueueParams = {}) =>\n eventManager.emit(Event.ClearWaitingQueue, params);\n\n/**\n * return true if one container is displaying the toast\n */\ntoast.isActive = (id: Id) => {\n let isToastActive = false;\n\n containers.forEach(container => {\n if (container.isToastActive && container.isToastActive(id)) {\n isToastActive = true;\n }\n });\n\n return isToastActive;\n};\n\ntoast.update = (toastId: Id, options: UpdateOptions = {}) => {\n // if you call toast and toast.update directly nothing will be displayed\n // this is why I defered the update\n setTimeout(() => {\n const toast = getToast(toastId, options as ToastOptions);\n if (toast) {\n const { props: oldOptions, content: oldContent } = toast;\n\n const nextOptions = {\n ...oldOptions,\n ...options,\n toastId: options.toastId || toastId,\n updateId: generateToastId()\n } as ToastProps & UpdateOptions;\n\n if (nextOptions.toastId !== toastId) nextOptions.staleId = toastId;\n\n const content = nextOptions.render || oldContent;\n delete nextOptions.render;\n\n dispatchToast(content, nextOptions);\n }\n }, 0);\n};\n\n/**\n * Used for controlled progress bar.\n */\ntoast.done = (id: Id) => {\n toast.update(id, {\n progress: 1\n });\n};\n\n/**\n * Track changes. The callback get the number of toast displayed\n *\n */\ntoast.onChange = (callback: OnChangeCallback) => {\n if (isFn(callback)) {\n eventManager.on(Event.Change, callback);\n }\n return () => {\n isFn(callback) && eventManager.off(Event.Change, callback);\n };\n};\n\n/**\n * Configure the ToastContainer when lazy mounted\n */\ntoast.configure = (config: ToastContainerProps = {}) => {\n lazy = true;\n containerConfig = config;\n};\n\ntoast.POSITION = POSITION;\ntoast.TYPE = TYPE;\n\n/**\n * Wait until the ToastContainer is mounted to dispatch the toast\n * and attach isActive method\n */\neventManager\n .on(Event.DidMount, (containerInstance: ContainerInstance) => {\n latestInstance = containerInstance.containerId || containerInstance;\n containers.set(latestInstance, containerInstance);\n\n queue.forEach(item => {\n eventManager.emit(Event.Show, item.content, item.options);\n });\n\n queue = [];\n })\n .on(Event.WillUnmount, (containerInstance: ContainerInstance) => {\n containers.delete(containerInstance.containerId || containerInstance);\n\n if (containers.size === 0) {\n eventManager\n .off(Event.Show)\n .off(Event.Clear)\n .off(Event.ClearWaitingQueue);\n }\n\n if (canUseDom && containerDomNode) {\n document.body.removeChild(containerDomNode);\n }\n });\n\nexport { toast };\n","/**\n * Copyright (c) 2013-present, Facebook, Inc.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nif (process.env.NODE_ENV !== 'production') {\n var ReactIs = require('react-is');\n\n // By explicitly using `prop-types` you are opting into new development behavior.\n // http://fb.me/prop-types-in-prod\n var throwOnDirectAccess = true;\n module.exports = require('./factoryWithTypeCheckers')(ReactIs.isElement, throwOnDirectAccess);\n} else {\n // By explicitly using `prop-types` you are opting into new production behavior.\n // http://fb.me/prop-types-in-prod\n module.exports = require('./factoryWithThrowingShims')();\n}\n","\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = findTabbableDescendants;\n/*!\n * Adapted from jQuery UI core\n *\n * http://jqueryui.com\n *\n * Copyright 2014 jQuery Foundation and other contributors\n * Released under the MIT license.\n * http://jquery.org/license\n *\n * http://api.jqueryui.com/category/ui-core/\n */\n\nvar tabbableNode = /input|select|textarea|button|object/;\n\nfunction hidesContents(element) {\n var zeroSize = element.offsetWidth <= 0 && element.offsetHeight <= 0;\n\n // If the node is empty, this is good enough\n if (zeroSize && !element.innerHTML) return true;\n\n try {\n // Otherwise we need to check some styles\n var style = window.getComputedStyle(element);\n return zeroSize ? style.getPropertyValue(\"overflow\") !== \"visible\" ||\n // if 'overflow: visible' set, check if there is actually any overflow\n element.scrollWidth <= 0 && element.scrollHeight <= 0 : style.getPropertyValue(\"display\") == \"none\";\n } catch (exception) {\n // eslint-disable-next-line no-console\n console.warn(\"Failed to inspect element style\");\n return false;\n }\n}\n\nfunction visible(element) {\n var parentElement = element;\n var rootNode = element.getRootNode && element.getRootNode();\n while (parentElement) {\n if (parentElement === document.body) break;\n\n // if we are not hidden yet, skip to checking outside the Web Component\n if (rootNode && parentElement === rootNode) parentElement = rootNode.host.parentNode;\n\n if (hidesContents(parentElement)) return false;\n parentElement = parentElement.parentNode;\n }\n return true;\n}\n\nfunction focusable(element, isTabIndexNotNaN) {\n var nodeName = element.nodeName.toLowerCase();\n var res = tabbableNode.test(nodeName) && !element.disabled || (nodeName === \"a\" ? element.href || isTabIndexNotNaN : isTabIndexNotNaN);\n return res && visible(element);\n}\n\nfunction tabbable(element) {\n var tabIndex = element.getAttribute(\"tabindex\");\n if (tabIndex === null) tabIndex = undefined;\n var isTabIndexNaN = isNaN(tabIndex);\n return (isTabIndexNaN || tabIndex >= 0) && focusable(element, !isTabIndexNaN);\n}\n\nfunction findTabbableDescendants(element) {\n var descendants = [].slice.call(element.querySelectorAll(\"*\"), 0).reduce(function (finished, el) {\n return finished.concat(!el.shadowRoot ? [el] : findTabbableDescendants(el.shadowRoot));\n }, []);\n return descendants.filter(tabbable);\n}\nmodule.exports = exports[\"default\"];","\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.resetState = resetState;\nexports.log = log;\nexports.assertNodeList = assertNodeList;\nexports.setElement = setElement;\nexports.validateElement = validateElement;\nexports.hide = hide;\nexports.show = show;\nexports.documentNotReadyOrSSRTesting = documentNotReadyOrSSRTesting;\n\nvar _warning = require(\"warning\");\n\nvar _warning2 = _interopRequireDefault(_warning);\n\nvar _safeHTMLElement = require(\"./safeHTMLElement\");\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nvar globalElement = null;\n\n/* eslint-disable no-console */\n/* istanbul ignore next */\nfunction resetState() {\n if (globalElement) {\n if (globalElement.removeAttribute) {\n globalElement.removeAttribute(\"aria-hidden\");\n } else if (globalElement.length != null) {\n globalElement.forEach(function (element) {\n return element.removeAttribute(\"aria-hidden\");\n });\n } else {\n document.querySelectorAll(globalElement).forEach(function (element) {\n return element.removeAttribute(\"aria-hidden\");\n });\n }\n }\n globalElement = null;\n}\n\n/* istanbul ignore next */\nfunction log() {\n if (process.env.NODE_ENV !== \"production\") {\n var check = globalElement || {};\n console.log(\"ariaAppHider ----------\");\n console.log(check.nodeName, check.className, check.id);\n console.log(\"end ariaAppHider ----------\");\n }\n}\n/* eslint-enable no-console */\n\nfunction assertNodeList(nodeList, selector) {\n if (!nodeList || !nodeList.length) {\n throw new Error(\"react-modal: No elements were found for selector \" + selector + \".\");\n }\n}\n\nfunction setElement(element) {\n var useElement = element;\n if (typeof useElement === \"string\" && _safeHTMLElement.canUseDOM) {\n var el = document.querySelectorAll(useElement);\n assertNodeList(el, useElement);\n useElement = el;\n }\n globalElement = useElement || globalElement;\n return globalElement;\n}\n\nfunction validateElement(appElement) {\n var el = appElement || globalElement;\n if (el) {\n return Array.isArray(el) || el instanceof HTMLCollection || el instanceof NodeList ? el : [el];\n } else {\n (0, _warning2.default)(false, [\"react-modal: App element is not defined.\", \"Please use `Modal.setAppElement(el)` or set `appElement={el}`.\", \"This is needed so screen readers don't see main content\", \"when modal is opened. It is not recommended, but you can opt-out\", \"by setting `ariaHideApp={false}`.\"].join(\" \"));\n\n return [];\n }\n}\n\nfunction hide(appElement) {\n var _iteratorNormalCompletion = true;\n var _didIteratorError = false;\n var _iteratorError = undefined;\n\n try {\n for (var _iterator = validateElement(appElement)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {\n var el = _step.value;\n\n el.setAttribute(\"aria-hidden\", \"true\");\n }\n } catch (err) {\n _didIteratorError = true;\n _iteratorError = err;\n } finally {\n try {\n if (!_iteratorNormalCompletion && _iterator.return) {\n _iterator.return();\n }\n } finally {\n if (_didIteratorError) {\n throw _iteratorError;\n }\n }\n }\n}\n\nfunction show(appElement) {\n var _iteratorNormalCompletion2 = true;\n var _didIteratorError2 = false;\n var _iteratorError2 = undefined;\n\n try {\n for (var _iterator2 = validateElement(appElement)[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {\n var el = _step2.value;\n\n el.removeAttribute(\"aria-hidden\");\n }\n } catch (err) {\n _didIteratorError2 = true;\n _iteratorError2 = err;\n } finally {\n try {\n if (!_iteratorNormalCompletion2 && _iterator2.return) {\n _iterator2.return();\n }\n } finally {\n if (_didIteratorError2) {\n throw _iteratorError2;\n }\n }\n }\n}\n\nfunction documentNotReadyOrSSRTesting() {\n globalElement = null;\n}","\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.log = log;\nexports.resetState = resetState;\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\n// Tracks portals that are open and emits events to subscribers\n\nvar PortalOpenInstances = function PortalOpenInstances() {\n var _this = this;\n\n _classCallCheck(this, PortalOpenInstances);\n\n this.register = function (openInstance) {\n if (_this.openInstances.indexOf(openInstance) !== -1) {\n if (process.env.NODE_ENV !== \"production\") {\n // eslint-disable-next-line no-console\n console.warn(\"React-Modal: Cannot register modal instance that's already open\");\n }\n return;\n }\n _this.openInstances.push(openInstance);\n _this.emit(\"register\");\n };\n\n this.deregister = function (openInstance) {\n var index = _this.openInstances.indexOf(openInstance);\n if (index === -1) {\n if (process.env.NODE_ENV !== \"production\") {\n // eslint-disable-next-line no-console\n console.warn(\"React-Modal: Unable to deregister \" + openInstance + \" as \" + \"it was never registered\");\n }\n return;\n }\n _this.openInstances.splice(index, 1);\n _this.emit(\"deregister\");\n };\n\n this.subscribe = function (callback) {\n _this.subscribers.push(callback);\n };\n\n this.emit = function (eventType) {\n _this.subscribers.forEach(function (subscriber) {\n return subscriber(eventType,\n // shallow copy to avoid accidental mutation\n _this.openInstances.slice());\n });\n };\n\n this.openInstances = [];\n this.subscribers = [];\n};\n\nvar portalOpenInstances = new PortalOpenInstances();\n\n/* eslint-disable no-console */\n/* istanbul ignore next */\nfunction log() {\n console.log(\"portalOpenInstances ----------\");\n console.log(portalOpenInstances.openInstances.length);\n portalOpenInstances.openInstances.forEach(function (p) {\n return console.log(p);\n });\n console.log(\"end portalOpenInstances ----------\");\n}\n\n/* istanbul ignore next */\nfunction resetState() {\n portalOpenInstances = new PortalOpenInstances();\n}\n/* eslint-enable no-console */\n\nexports.default = portalOpenInstances;","'use strict';\n\nmodule.exports = function bind(fn, thisArg) {\n return function wrap() {\n var args = new Array(arguments.length);\n for (var i = 0; i < args.length; i++) {\n args[i] = arguments[i];\n }\n return fn.apply(thisArg, args);\n };\n};\n","'use strict';\n\nvar utils = require('./../utils');\n\nfunction encode(val) {\n return encodeURIComponent(val).\n replace(/%3A/gi, ':').\n replace(/%24/g, '$').\n replace(/%2C/gi, ',').\n replace(/%20/g, '+').\n replace(/%5B/gi, '[').\n replace(/%5D/gi, ']');\n}\n\n/**\n * Build a URL by appending params to the end\n *\n * @param {string} url The base of the url (e.g., http://www.google.com)\n * @param {object} [params] The params to be appended\n * @returns {string} The formatted url\n */\nmodule.exports = function buildURL(url, params, paramsSerializer) {\n /*eslint no-param-reassign:0*/\n if (!params) {\n return url;\n }\n\n var serializedParams;\n if (paramsSerializer) {\n serializedParams = paramsSerializer(params);\n } else if (utils.isURLSearchParams(params)) {\n serializedParams = params.toString();\n } else {\n var parts = [];\n\n utils.forEach(params, function serialize(val, key) {\n if (val === null || typeof val === 'undefined') {\n return;\n }\n\n if (utils.isArray(val)) {\n key = key + '[]';\n } else {\n val = [val];\n }\n\n utils.forEach(val, function parseValue(v) {\n if (utils.isDate(v)) {\n v = v.toISOString();\n } else if (utils.isObject(v)) {\n v = JSON.stringify(v);\n }\n parts.push(encode(key) + '=' + encode(v));\n });\n });\n\n serializedParams = parts.join('&');\n }\n\n if (serializedParams) {\n var hashmarkIndex = url.indexOf('#');\n if (hashmarkIndex !== -1) {\n url = url.slice(0, hashmarkIndex);\n }\n\n url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams;\n }\n\n return url;\n};\n","'use strict';\n\n/**\n * Update an Error with the specified config, error code, and response.\n *\n * @param {Error} error The error to update.\n * @param {Object} config The config.\n * @param {string} [code] The error code (for example, 'ECONNABORTED').\n * @param {Object} [request] The request.\n * @param {Object} [response] The response.\n * @returns {Error} The error.\n */\nmodule.exports = function enhanceError(error, config, code, request, response) {\n error.config = config;\n if (code) {\n error.code = code;\n }\n\n error.request = request;\n error.response = response;\n error.isAxiosError = true;\n\n error.toJSON = function toJSON() {\n return {\n // Standard\n message: this.message,\n name: this.name,\n // Microsoft\n description: this.description,\n number: this.number,\n // Mozilla\n fileName: this.fileName,\n lineNumber: this.lineNumber,\n columnNumber: this.columnNumber,\n stack: this.stack,\n // Axios\n config: this.config,\n code: this.code,\n status: this.response && this.response.status ? this.response.status : null\n };\n };\n return error;\n};\n","'use strict';\n\nvar utils = require('./../utils');\nvar settle = require('./../core/settle');\nvar cookies = require('./../helpers/cookies');\nvar buildURL = require('./../helpers/buildURL');\nvar buildFullPath = require('../core/buildFullPath');\nvar parseHeaders = require('./../helpers/parseHeaders');\nvar isURLSameOrigin = require('./../helpers/isURLSameOrigin');\nvar createError = require('../core/createError');\nvar defaults = require('../defaults');\nvar Cancel = require('../cancel/Cancel');\n\nmodule.exports = function xhrAdapter(config) {\n return new Promise(function dispatchXhrRequest(resolve, reject) {\n var requestData = config.data;\n var requestHeaders = config.headers;\n var responseType = config.responseType;\n var onCanceled;\n function done() {\n if (config.cancelToken) {\n config.cancelToken.unsubscribe(onCanceled);\n }\n\n if (config.signal) {\n config.signal.removeEventListener('abort', onCanceled);\n }\n }\n\n if (utils.isFormData(requestData)) {\n delete requestHeaders['Content-Type']; // Let the browser set it\n }\n\n var request = new XMLHttpRequest();\n\n // HTTP basic authentication\n if (config.auth) {\n var username = config.auth.username || '';\n var password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : '';\n requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);\n }\n\n var fullPath = buildFullPath(config.baseURL, config.url);\n request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);\n\n // Set the request timeout in MS\n request.timeout = config.timeout;\n\n function onloadend() {\n if (!request) {\n return;\n }\n // Prepare the response\n var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;\n var responseData = !responseType || responseType === 'text' || responseType === 'json' ?\n request.responseText : request.response;\n var response = {\n data: responseData,\n status: request.status,\n statusText: request.statusText,\n headers: responseHeaders,\n config: config,\n request: request\n };\n\n settle(function _resolve(value) {\n resolve(value);\n done();\n }, function _reject(err) {\n reject(err);\n done();\n }, response);\n\n // Clean up request\n request = null;\n }\n\n if ('onloadend' in request) {\n // Use onloadend if available\n request.onloadend = onloadend;\n } else {\n // Listen for ready state to emulate onloadend\n request.onreadystatechange = function handleLoad() {\n if (!request || request.readyState !== 4) {\n return;\n }\n\n // The request errored out and we didn't get a response, this will be\n // handled by onerror instead\n // With one exception: request that using file: protocol, most browsers\n // will return status as 0 even though it's a successful request\n if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {\n return;\n }\n // readystate handler is calling before onerror or ontimeout handlers,\n // so we should call onloadend on the next 'tick'\n setTimeout(onloadend);\n };\n }\n\n // Handle browser request cancellation (as opposed to a manual cancellation)\n request.onabort = function handleAbort() {\n if (!request) {\n return;\n }\n\n reject(createError('Request aborted', config, 'ECONNABORTED', request));\n\n // Clean up request\n request = null;\n };\n\n // Handle low level network errors\n request.onerror = function handleError() {\n // Real errors are hidden from us by the browser\n // onerror should only fire if it's a network error\n reject(createError('Network Error', config, null, request));\n\n // Clean up request\n request = null;\n };\n\n // Handle timeout\n request.ontimeout = function handleTimeout() {\n var timeoutErrorMessage = config.timeout ? 'timeout of ' + config.timeout + 'ms exceeded' : 'timeout exceeded';\n var transitional = config.transitional || defaults.transitional;\n if (config.timeoutErrorMessage) {\n timeoutErrorMessage = config.timeoutErrorMessage;\n }\n reject(createError(\n timeoutErrorMessage,\n config,\n transitional.clarifyTimeoutError ? 'ETIMEDOUT' : 'ECONNABORTED',\n request));\n\n // Clean up request\n request = null;\n };\n\n // Add xsrf header\n // This is only done if running in a standard browser environment.\n // Specifically not if we're in a web worker, or react-native.\n if (utils.isStandardBrowserEnv()) {\n // Add xsrf header\n var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?\n cookies.read(config.xsrfCookieName) :\n undefined;\n\n if (xsrfValue) {\n requestHeaders[config.xsrfHeaderName] = xsrfValue;\n }\n }\n\n // Add headers to the request\n if ('setRequestHeader' in request) {\n utils.forEach(requestHeaders, function setRequestHeader(val, key) {\n if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {\n // Remove Content-Type if data is undefined\n delete requestHeaders[key];\n } else {\n // Otherwise add header to the request\n request.setRequestHeader(key, val);\n }\n });\n }\n\n // Add withCredentials to request if needed\n if (!utils.isUndefined(config.withCredentials)) {\n request.withCredentials = !!config.withCredentials;\n }\n\n // Add responseType to request if needed\n if (responseType && responseType !== 'json') {\n request.responseType = config.responseType;\n }\n\n // Handle progress if needed\n if (typeof config.onDownloadProgress === 'function') {\n request.addEventListener('progress', config.onDownloadProgress);\n }\n\n // Not all browsers support upload events\n if (typeof config.onUploadProgress === 'function' && request.upload) {\n request.upload.addEventListener('progress', config.onUploadProgress);\n }\n\n if (config.cancelToken || config.signal) {\n // Handle cancellation\n // eslint-disable-next-line func-names\n onCanceled = function(cancel) {\n if (!request) {\n return;\n }\n reject(!cancel || (cancel && cancel.type) ? new Cancel('canceled') : cancel);\n request.abort();\n request = null;\n };\n\n config.cancelToken && config.cancelToken.subscribe(onCanceled);\n if (config.signal) {\n config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);\n }\n }\n\n if (!requestData) {\n requestData = null;\n }\n\n // Send the request\n request.send(requestData);\n });\n};\n","'use strict';\n\nvar enhanceError = require('./enhanceError');\n\n/**\n * Create an Error with the specified message, config, error code, request and response.\n *\n * @param {string} message The error message.\n * @param {Object} config The config.\n * @param {string} [code] The error code (for example, 'ECONNABORTED').\n * @param {Object} [request] The request.\n * @param {Object} [response] The response.\n * @returns {Error} The created error.\n */\nmodule.exports = function createError(message, config, code, request, response) {\n var error = new Error(message);\n return enhanceError(error, config, code, request, response);\n};\n","'use strict';\n\nmodule.exports = function isCancel(value) {\n return !!(value && value.__CANCEL__);\n};\n","'use strict';\n\nvar utils = require('../utils');\n\n/**\n * Config-specific merge-function which creates a new config-object\n * by merging two configuration objects together.\n *\n * @param {Object} config1\n * @param {Object} config2\n * @returns {Object} New object resulting from merging config2 to config1\n */\nmodule.exports = function mergeConfig(config1, config2) {\n // eslint-disable-next-line no-param-reassign\n config2 = config2 || {};\n var config = {};\n\n function getMergedValue(target, source) {\n if (utils.isPlainObject(target) && utils.isPlainObject(source)) {\n return utils.merge(target, source);\n } else if (utils.isPlainObject(source)) {\n return utils.merge({}, source);\n } else if (utils.isArray(source)) {\n return source.slice();\n }\n return source;\n }\n\n // eslint-disable-next-line consistent-return\n function mergeDeepProperties(prop) {\n if (!utils.isUndefined(config2[prop])) {\n return getMergedValue(config1[prop], config2[prop]);\n } else if (!utils.isUndefined(config1[prop])) {\n return getMergedValue(undefined, config1[prop]);\n }\n }\n\n // eslint-disable-next-line consistent-return\n function valueFromConfig2(prop) {\n if (!utils.isUndefined(config2[prop])) {\n return getMergedValue(undefined, config2[prop]);\n }\n }\n\n // eslint-disable-next-line consistent-return\n function defaultToConfig2(prop) {\n if (!utils.isUndefined(config2[prop])) {\n return getMergedValue(undefined, config2[prop]);\n } else if (!utils.isUndefined(config1[prop])) {\n return getMergedValue(undefined, config1[prop]);\n }\n }\n\n // eslint-disable-next-line consistent-return\n function mergeDirectKeys(prop) {\n if (prop in config2) {\n return getMergedValue(config1[prop], config2[prop]);\n } else if (prop in config1) {\n return getMergedValue(undefined, config1[prop]);\n }\n }\n\n var mergeMap = {\n 'url': valueFromConfig2,\n 'method': valueFromConfig2,\n 'data': valueFromConfig2,\n 'baseURL': defaultToConfig2,\n 'transformRequest': defaultToConfig2,\n 'transformResponse': defaultToConfig2,\n 'paramsSerializer': defaultToConfig2,\n 'timeout': defaultToConfig2,\n 'timeoutMessage': defaultToConfig2,\n 'withCredentials': defaultToConfig2,\n 'adapter': defaultToConfig2,\n 'responseType': defaultToConfig2,\n 'xsrfCookieName': defaultToConfig2,\n 'xsrfHeaderName': defaultToConfig2,\n 'onUploadProgress': defaultToConfig2,\n 'onDownloadProgress': defaultToConfig2,\n 'decompress': defaultToConfig2,\n 'maxContentLength': defaultToConfig2,\n 'maxBodyLength': defaultToConfig2,\n 'transport': defaultToConfig2,\n 'httpAgent': defaultToConfig2,\n 'httpsAgent': defaultToConfig2,\n 'cancelToken': defaultToConfig2,\n 'socketPath': defaultToConfig2,\n 'responseEncoding': defaultToConfig2,\n 'validateStatus': mergeDirectKeys\n };\n\n utils.forEach(Object.keys(config1).concat(Object.keys(config2)), function computeConfigValue(prop) {\n var merge = mergeMap[prop] || mergeDeepProperties;\n var configValue = merge(prop);\n (utils.isUndefined(configValue) && merge !== mergeDirectKeys) || (config[prop] = configValue);\n });\n\n return config;\n};\n","module.exports = {\n \"version\": \"0.24.0\"\n};","/* @flow */\n/*::\n\ntype DotenvParseOptions = {\n debug?: boolean\n}\n\n// keys and values from src\ntype DotenvParseOutput = { [string]: string }\n\ntype DotenvConfigOptions = {\n path?: string, // path to .env file\n encoding?: string, // encoding of .env file\n debug?: string // turn on logging for debugging purposes\n}\n\ntype DotenvConfigOutput = {\n parsed?: DotenvParseOutput,\n error?: Error\n}\n\n*/\n\nconst fs = require('fs')\nconst path = require('path')\nconst os = require('os')\n\nfunction log (message /*: string */) {\n console.log(`[dotenv][DEBUG] ${message}`)\n}\n\nconst NEWLINE = '\\n'\nconst RE_INI_KEY_VAL = /^\\s*([\\w.-]+)\\s*=\\s*(.*)?\\s*$/\nconst RE_NEWLINES = /\\\\n/g\nconst NEWLINES_MATCH = /\\r\\n|\\n|\\r/\n\n// Parses src into an Object\nfunction parse (src /*: string | Buffer */, options /*: ?DotenvParseOptions */) /*: DotenvParseOutput */ {\n const debug = Boolean(options && options.debug)\n const obj = {}\n\n // convert Buffers before splitting into lines and processing\n src.toString().split(NEWLINES_MATCH).forEach(function (line, idx) {\n // matching \"KEY' and 'VAL' in 'KEY=VAL'\n const keyValueArr = line.match(RE_INI_KEY_VAL)\n // matched?\n if (keyValueArr != null) {\n const key = keyValueArr[1]\n // default undefined or missing values to empty string\n let val = (keyValueArr[2] || '')\n const end = val.length - 1\n const isDoubleQuoted = val[0] === '\"' && val[end] === '\"'\n const isSingleQuoted = val[0] === \"'\" && val[end] === \"'\"\n\n // if single or double quoted, remove quotes\n if (isSingleQuoted || isDoubleQuoted) {\n val = val.substring(1, end)\n\n // if double quoted, expand newlines\n if (isDoubleQuoted) {\n val = val.replace(RE_NEWLINES, NEWLINE)\n }\n } else {\n // remove surrounding whitespace\n val = val.trim()\n }\n\n obj[key] = val\n } else if (debug) {\n log(`did not match key and value when parsing line ${idx + 1}: ${line}`)\n }\n })\n\n return obj\n}\n\nfunction resolveHome (envPath) {\n return envPath[0] === '~' ? path.join(os.homedir(), envPath.slice(1)) : envPath\n}\n\n// Populates process.env from .env file\nfunction config (options /*: ?DotenvConfigOptions */) /*: DotenvConfigOutput */ {\n let dotenvPath = path.resolve(process.cwd(), '.env')\n let encoding /*: string */ = 'utf8'\n let debug = false\n\n if (options) {\n if (options.path != null) {\n dotenvPath = resolveHome(options.path)\n }\n if (options.encoding != null) {\n encoding = options.encoding\n }\n if (options.debug != null) {\n debug = true\n }\n }\n\n try {\n // specifying an encoding returns a string instead of a buffer\n const parsed = parse(fs.readFileSync(dotenvPath, { encoding }), { debug })\n\n Object.keys(parsed).forEach(function (key) {\n if (!Object.prototype.hasOwnProperty.call(process.env, key)) {\n process.env[key] = parsed[key]\n } else if (debug) {\n log(`\"${key}\" is already defined in \\`process.env\\` and will not be overwritten`)\n }\n })\n\n return { parsed }\n } catch (e) {\n return { error: e }\n }\n}\n\nmodule.exports.config = config\nmodule.exports.parse = parse\n","\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n\nvar _Modal = require(\"./components/Modal\");\n\nvar _Modal2 = _interopRequireDefault(_Modal);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nexports.default = _Modal2.default;\nmodule.exports = exports[\"default\"];","module.exports = require('./lib/axios');","import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport { LeafletProvider } from './context';\nexport function createContainerComponent(useElement) {\n function ContainerComponent(props, ref) {\n const {\n instance,\n context\n } = useElement(props).current;\n useImperativeHandle(ref, () => instance);\n return props.children == null ? null : /*#__PURE__*/React.createElement(LeafletProvider, {\n value: context\n }, props.children);\n }\n\n return /*#__PURE__*/forwardRef(ContainerComponent);\n}\nexport function createDivOverlayComponent(useElement) {\n function OverlayComponent(props, ref) {\n const [isOpen, setOpen] = useState(false);\n const {\n instance\n } = useElement(props, setOpen).current;\n useImperativeHandle(ref, () => instance);\n useEffect(function updateOverlay() {\n if (isOpen) {\n instance.update();\n }\n }, [instance, isOpen, props.children]); // @ts-ignore _contentNode missing in type definition\n\n const contentNode = instance._contentNode;\n return contentNode ? /*#__PURE__*/createPortal(props.children, contentNode) : null;\n }\n\n return /*#__PURE__*/forwardRef(OverlayComponent);\n}\nexport function createLeafComponent(useElement) {\n function LeafComponent(props, ref) {\n const {\n instance\n } = useElement(props).current;\n useImperativeHandle(ref, () => instance);\n return null;\n }\n\n return /*#__PURE__*/forwardRef(LeafComponent);\n}","import { useEffect, useRef } from 'react';\nexport function createElementHook(createElement, updateElement) {\n if (updateElement == null) {\n return function useImmutableLeafletElement(props, context) {\n return useRef(createElement(props, context));\n };\n }\n\n return function useMutableLeafletElement(props, context) {\n const elementRef = useRef(createElement(props, context));\n const propsRef = useRef(props);\n const {\n instance\n } = elementRef.current;\n useEffect(function updateElementProps() {\n if (propsRef.current !== props) {\n updateElement(instance, props, propsRef.current);\n propsRef.current = props;\n }\n }, [instance, props, context]);\n return elementRef;\n };\n}","import { useEffect, useRef } from 'react';\nexport function useAttribution(map, attribution) {\n const attributionRef = useRef(attribution);\n useEffect(function updateAttribution() {\n if (attribution !== attributionRef.current && map.attributionControl != null) {\n if (attributionRef.current != null) {\n map.attributionControl.removeAttribution(attributionRef.current);\n }\n\n if (attribution != null) {\n map.attributionControl.addAttribution(attribution);\n }\n }\n\n attributionRef.current = attribution;\n }, [map, attribution]);\n}","import { useEffect, useRef } from 'react';\nexport function useEventHandlers(element, eventHandlers) {\n const eventHandlersRef = useRef();\n useEffect(function addEventHandlers() {\n if (eventHandlers != null) {\n element.instance.on(eventHandlers);\n }\n\n eventHandlersRef.current = eventHandlers;\n return function removeEventHandlers() {\n if (eventHandlersRef.current != null) {\n element.instance.off(eventHandlersRef.current);\n }\n\n eventHandlersRef.current = null;\n };\n }, [element, eventHandlers]);\n}","import { useEffect } from 'react';\nimport { useAttribution } from './attribution';\nimport { useLeafletContext } from './context';\nimport { useEventHandlers } from './events';\nimport { withPane } from './pane';\nexport function useLayerLifecycle(element, context) {\n useEffect(function addLayer() {\n const container = context.layerContainer ?? context.map;\n container.addLayer(element.instance);\n return function removeLayer() {\n var _context$layerContain;\n\n (_context$layerContain = context.layerContainer) == null ? void 0 : _context$layerContain.removeLayer(element.instance);\n context.map.removeLayer(element.instance);\n };\n }, [context, element]);\n}\nexport function createLayerHook(useElement) {\n return function useLayer(props) {\n const context = useLeafletContext();\n const elementRef = useElement(withPane(props, context), context);\n useAttribution(context.map, props.attribution);\n useEventHandlers(elementRef.current, props.eventHandlers);\n useLayerLifecycle(elementRef.current, context);\n return elementRef;\n };\n}","import { createContainerComponent, createDivOverlayComponent, createLeafComponent } from './component';\nimport { createControlHook } from './control';\nimport { createElementHook } from './element';\nimport { createLayerHook } from './layer';\nimport { createDivOverlayHook } from './div-overlay';\nimport { createPathHook } from './path';\nexport function createControlComponent(createInstance) {\n function createElement(props, context) {\n return {\n instance: createInstance(props),\n context\n };\n }\n\n const useElement = createElementHook(createElement);\n const useControl = createControlHook(useElement);\n return createLeafComponent(useControl);\n}\nexport function createLayerComponent(createElement, updateElement) {\n const useElement = createElementHook(createElement, updateElement);\n const useLayer = createLayerHook(useElement);\n return createContainerComponent(useLayer);\n}\nexport function createOverlayComponent(createElement, useLifecycle) {\n const useElement = createElementHook(createElement);\n const useOverlay = createDivOverlayHook(useElement, useLifecycle);\n return createDivOverlayComponent(useOverlay);\n}\nexport function createPathComponent(createElement, updateElement) {\n const useElement = createElementHook(createElement, updateElement);\n const usePath = createPathHook(useElement);\n return createContainerComponent(usePath);\n}\nexport function createTileLayerComponent(createElement, updateElement) {\n const useElement = createElementHook(createElement, updateElement);\n const useLayer = createLayerHook(useElement);\n return createLeafComponent(useLayer);\n}","import { useAttribution } from './attribution';\nimport { useLeafletContext } from './context';\nimport { useEventHandlers } from './events';\nimport { withPane } from './pane';\nexport function createDivOverlayHook(useElement, useLifecycle) {\n return function useDivOverlay(props, setOpen) {\n const context = useLeafletContext();\n const elementRef = useElement(withPane(props, context), context);\n useAttribution(context.map, props.attribution);\n useEventHandlers(elementRef.current, props.eventHandlers);\n useLifecycle(elementRef.current, context, props, setOpen);\n return elementRef;\n };\n}","/** @license React v17.0.2\n * react.production.min.js\n *\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n'use strict';var l=require(\"object-assign\"),n=60103,p=60106;exports.Fragment=60107;exports.StrictMode=60108;exports.Profiler=60114;var q=60109,r=60110,t=60112;exports.Suspense=60113;var u=60115,v=60116;\nif(\"function\"===typeof Symbol&&Symbol.for){var w=Symbol.for;n=w(\"react.element\");p=w(\"react.portal\");exports.Fragment=w(\"react.fragment\");exports.StrictMode=w(\"react.strict_mode\");exports.Profiler=w(\"react.profiler\");q=w(\"react.provider\");r=w(\"react.context\");t=w(\"react.forward_ref\");exports.Suspense=w(\"react.suspense\");u=w(\"react.memo\");v=w(\"react.lazy\")}var x=\"function\"===typeof Symbol&&Symbol.iterator;\nfunction y(a){if(null===a||\"object\"!==typeof a)return null;a=x&&a[x]||a[\"@@iterator\"];return\"function\"===typeof a?a:null}function z(a){for(var b=\"https://reactjs.org/docs/error-decoder.html?invariant=\"+a,c=1;cb}return!1}function B(a,b,c,d,e,f,g){this.acceptsBooleans=2===b||3===b||4===b;this.attributeName=d;this.attributeNamespace=e;this.mustUseProperty=c;this.propertyName=a;this.type=b;this.sanitizeURL=f;this.removeEmptyString=g}var D={};\n\"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style\".split(\" \").forEach(function(a){D[a]=new B(a,0,!1,a,null,!1,!1)});[[\"acceptCharset\",\"accept-charset\"],[\"className\",\"class\"],[\"htmlFor\",\"for\"],[\"httpEquiv\",\"http-equiv\"]].forEach(function(a){var b=a[0];D[b]=new B(b,1,!1,a[1],null,!1,!1)});[\"contentEditable\",\"draggable\",\"spellCheck\",\"value\"].forEach(function(a){D[a]=new B(a,2,!1,a.toLowerCase(),null,!1,!1)});\n[\"autoReverse\",\"externalResourcesRequired\",\"focusable\",\"preserveAlpha\"].forEach(function(a){D[a]=new B(a,2,!1,a,null,!1,!1)});\"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope\".split(\" \").forEach(function(a){D[a]=new B(a,3,!1,a.toLowerCase(),null,!1,!1)});\n[\"checked\",\"multiple\",\"muted\",\"selected\"].forEach(function(a){D[a]=new B(a,3,!0,a,null,!1,!1)});[\"capture\",\"download\"].forEach(function(a){D[a]=new B(a,4,!1,a,null,!1,!1)});[\"cols\",\"rows\",\"size\",\"span\"].forEach(function(a){D[a]=new B(a,6,!1,a,null,!1,!1)});[\"rowSpan\",\"start\"].forEach(function(a){D[a]=new B(a,5,!1,a.toLowerCase(),null,!1,!1)});var oa=/[\\-:]([a-z])/g;function pa(a){return a[1].toUpperCase()}\n\"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height\".split(\" \").forEach(function(a){var b=a.replace(oa,\npa);D[b]=new B(b,1,!1,a,null,!1,!1)});\"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type\".split(\" \").forEach(function(a){var b=a.replace(oa,pa);D[b]=new B(b,1,!1,a,\"http://www.w3.org/1999/xlink\",!1,!1)});[\"xml:base\",\"xml:lang\",\"xml:space\"].forEach(function(a){var b=a.replace(oa,pa);D[b]=new B(b,1,!1,a,\"http://www.w3.org/XML/1998/namespace\",!1,!1)});[\"tabIndex\",\"crossOrigin\"].forEach(function(a){D[a]=new B(a,1,!1,a.toLowerCase(),null,!1,!1)});\nD.xlinkHref=new B(\"xlinkHref\",1,!1,\"xlink:href\",\"http://www.w3.org/1999/xlink\",!0,!1);[\"src\",\"href\",\"action\",\"formAction\"].forEach(function(a){D[a]=new B(a,1,!1,a.toLowerCase(),null,!0,!0)});\nfunction qa(a,b,c,d){var e=D.hasOwnProperty(b)?D[b]:null;var f=null!==e?0===e.type:d?!1:!(2h||e[g]!==f[h])return\"\\n\"+e[g].replace(\" at new \",\" at \");while(1<=g&&0<=h)}break}}}finally{Oa=!1,Error.prepareStackTrace=c}return(a=a?a.displayName||a.name:\"\")?Na(a):\"\"}\nfunction Qa(a){switch(a.tag){case 5:return Na(a.type);case 16:return Na(\"Lazy\");case 13:return Na(\"Suspense\");case 19:return Na(\"SuspenseList\");case 0:case 2:case 15:return a=Pa(a.type,!1),a;case 11:return a=Pa(a.type.render,!1),a;case 22:return a=Pa(a.type._render,!1),a;case 1:return a=Pa(a.type,!0),a;default:return\"\"}}\nfunction Ra(a){if(null==a)return null;if(\"function\"===typeof a)return a.displayName||a.name||null;if(\"string\"===typeof a)return a;switch(a){case ua:return\"Fragment\";case ta:return\"Portal\";case xa:return\"Profiler\";case wa:return\"StrictMode\";case Ba:return\"Suspense\";case Ca:return\"SuspenseList\"}if(\"object\"===typeof a)switch(a.$$typeof){case za:return(a.displayName||\"Context\")+\".Consumer\";case ya:return(a._context.displayName||\"Context\")+\".Provider\";case Aa:var b=a.render;b=b.displayName||b.name||\"\";\nreturn a.displayName||(\"\"!==b?\"ForwardRef(\"+b+\")\":\"ForwardRef\");case Da:return Ra(a.type);case Fa:return Ra(a._render);case Ea:b=a._payload;a=a._init;try{return Ra(a(b))}catch(c){}}return null}function Sa(a){switch(typeof a){case \"boolean\":case \"number\":case \"object\":case \"string\":case \"undefined\":return a;default:return\"\"}}function Ta(a){var b=a.type;return(a=a.nodeName)&&\"input\"===a.toLowerCase()&&(\"checkbox\"===b||\"radio\"===b)}\nfunction Ua(a){var b=Ta(a)?\"checked\":\"value\",c=Object.getOwnPropertyDescriptor(a.constructor.prototype,b),d=\"\"+a[b];if(!a.hasOwnProperty(b)&&\"undefined\"!==typeof c&&\"function\"===typeof c.get&&\"function\"===typeof c.set){var e=c.get,f=c.set;Object.defineProperty(a,b,{configurable:!0,get:function(){return e.call(this)},set:function(a){d=\"\"+a;f.call(this,a)}});Object.defineProperty(a,b,{enumerable:c.enumerable});return{getValue:function(){return d},setValue:function(a){d=\"\"+a},stopTracking:function(){a._valueTracker=\nnull;delete a[b]}}}}function Va(a){a._valueTracker||(a._valueTracker=Ua(a))}function Wa(a){if(!a)return!1;var b=a._valueTracker;if(!b)return!0;var c=b.getValue();var d=\"\";a&&(d=Ta(a)?a.checked?\"true\":\"false\":a.value);a=d;return a!==c?(b.setValue(a),!0):!1}function Xa(a){a=a||(\"undefined\"!==typeof document?document:void 0);if(\"undefined\"===typeof a)return null;try{return a.activeElement||a.body}catch(b){return a.body}}\nfunction Ya(a,b){var c=b.checked;return m({},b,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:null!=c?c:a._wrapperState.initialChecked})}function Za(a,b){var c=null==b.defaultValue?\"\":b.defaultValue,d=null!=b.checked?b.checked:b.defaultChecked;c=Sa(null!=b.value?b.value:c);a._wrapperState={initialChecked:d,initialValue:c,controlled:\"checkbox\"===b.type||\"radio\"===b.type?null!=b.checked:null!=b.value}}function $a(a,b){b=b.checked;null!=b&&qa(a,\"checked\",b,!1)}\nfunction ab(a,b){$a(a,b);var c=Sa(b.value),d=b.type;if(null!=c)if(\"number\"===d){if(0===c&&\"\"===a.value||a.value!=c)a.value=\"\"+c}else a.value!==\"\"+c&&(a.value=\"\"+c);else if(\"submit\"===d||\"reset\"===d){a.removeAttribute(\"value\");return}b.hasOwnProperty(\"value\")?bb(a,b.type,c):b.hasOwnProperty(\"defaultValue\")&&bb(a,b.type,Sa(b.defaultValue));null==b.checked&&null!=b.defaultChecked&&(a.defaultChecked=!!b.defaultChecked)}\nfunction cb(a,b,c){if(b.hasOwnProperty(\"value\")||b.hasOwnProperty(\"defaultValue\")){var d=b.type;if(!(\"submit\"!==d&&\"reset\"!==d||void 0!==b.value&&null!==b.value))return;b=\"\"+a._wrapperState.initialValue;c||b===a.value||(a.value=b);a.defaultValue=b}c=a.name;\"\"!==c&&(a.name=\"\");a.defaultChecked=!!a._wrapperState.initialChecked;\"\"!==c&&(a.name=c)}\nfunction bb(a,b,c){if(\"number\"!==b||Xa(a.ownerDocument)!==a)null==c?a.defaultValue=\"\"+a._wrapperState.initialValue:a.defaultValue!==\"\"+c&&(a.defaultValue=\"\"+c)}function db(a){var b=\"\";aa.Children.forEach(a,function(a){null!=a&&(b+=a)});return b}function eb(a,b){a=m({children:void 0},b);if(b=db(b.children))a.children=b;return a}\nfunction fb(a,b,c,d){a=a.options;if(b){b={};for(var e=0;e=c.length))throw Error(y(93));c=c[0]}b=c}null==b&&(b=\"\");c=b}a._wrapperState={initialValue:Sa(c)}}\nfunction ib(a,b){var c=Sa(b.value),d=Sa(b.defaultValue);null!=c&&(c=\"\"+c,c!==a.value&&(a.value=c),null==b.defaultValue&&a.defaultValue!==c&&(a.defaultValue=c));null!=d&&(a.defaultValue=\"\"+d)}function jb(a){var b=a.textContent;b===a._wrapperState.initialValue&&\"\"!==b&&null!==b&&(a.value=b)}var kb={html:\"http://www.w3.org/1999/xhtml\",mathml:\"http://www.w3.org/1998/Math/MathML\",svg:\"http://www.w3.org/2000/svg\"};\nfunction lb(a){switch(a){case \"svg\":return\"http://www.w3.org/2000/svg\";case \"math\":return\"http://www.w3.org/1998/Math/MathML\";default:return\"http://www.w3.org/1999/xhtml\"}}function mb(a,b){return null==a||\"http://www.w3.org/1999/xhtml\"===a?lb(b):\"http://www.w3.org/2000/svg\"===a&&\"foreignObject\"===b?\"http://www.w3.org/1999/xhtml\":a}\nvar nb,ob=function(a){return\"undefined\"!==typeof MSApp&&MSApp.execUnsafeLocalFunction?function(b,c,d,e){MSApp.execUnsafeLocalFunction(function(){return a(b,c,d,e)})}:a}(function(a,b){if(a.namespaceURI!==kb.svg||\"innerHTML\"in a)a.innerHTML=b;else{nb=nb||document.createElement(\"div\");nb.innerHTML=\"\"+b.valueOf().toString()+\"\";for(b=nb.firstChild;a.firstChild;)a.removeChild(a.firstChild);for(;b.firstChild;)a.appendChild(b.firstChild)}});\nfunction pb(a,b){if(b){var c=a.firstChild;if(c&&c===a.lastChild&&3===c.nodeType){c.nodeValue=b;return}}a.textContent=b}\nvar qb={animationIterationCount:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,\nfloodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},rb=[\"Webkit\",\"ms\",\"Moz\",\"O\"];Object.keys(qb).forEach(function(a){rb.forEach(function(b){b=b+a.charAt(0).toUpperCase()+a.substring(1);qb[b]=qb[a]})});function sb(a,b,c){return null==b||\"boolean\"===typeof b||\"\"===b?\"\":c||\"number\"!==typeof b||0===b||qb.hasOwnProperty(a)&&qb[a]?(\"\"+b).trim():b+\"px\"}\nfunction tb(a,b){a=a.style;for(var c in b)if(b.hasOwnProperty(c)){var d=0===c.indexOf(\"--\"),e=sb(c,b[c],d);\"float\"===c&&(c=\"cssFloat\");d?a.setProperty(c,e):a[c]=e}}var ub=m({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});\nfunction vb(a,b){if(b){if(ub[a]&&(null!=b.children||null!=b.dangerouslySetInnerHTML))throw Error(y(137,a));if(null!=b.dangerouslySetInnerHTML){if(null!=b.children)throw Error(y(60));if(!(\"object\"===typeof b.dangerouslySetInnerHTML&&\"__html\"in b.dangerouslySetInnerHTML))throw Error(y(61));}if(null!=b.style&&\"object\"!==typeof b.style)throw Error(y(62));}}\nfunction wb(a,b){if(-1===a.indexOf(\"-\"))return\"string\"===typeof b.is;switch(a){case \"annotation-xml\":case \"color-profile\":case \"font-face\":case \"font-face-src\":case \"font-face-uri\":case \"font-face-format\":case \"font-face-name\":case \"missing-glyph\":return!1;default:return!0}}function xb(a){a=a.target||a.srcElement||window;a.correspondingUseElement&&(a=a.correspondingUseElement);return 3===a.nodeType?a.parentNode:a}var yb=null,zb=null,Ab=null;\nfunction Bb(a){if(a=Cb(a)){if(\"function\"!==typeof yb)throw Error(y(280));var b=a.stateNode;b&&(b=Db(b),yb(a.stateNode,a.type,b))}}function Eb(a){zb?Ab?Ab.push(a):Ab=[a]:zb=a}function Fb(){if(zb){var a=zb,b=Ab;Ab=zb=null;Bb(a);if(b)for(a=0;ad?0:1<c;c++)b.push(a);return b}\nfunction $c(a,b,c){a.pendingLanes|=b;var d=b-1;a.suspendedLanes&=d;a.pingedLanes&=d;a=a.eventTimes;b=31-Vc(b);a[b]=c}var Vc=Math.clz32?Math.clz32:ad,bd=Math.log,cd=Math.LN2;function ad(a){return 0===a?32:31-(bd(a)/cd|0)|0}var dd=r.unstable_UserBlockingPriority,ed=r.unstable_runWithPriority,fd=!0;function gd(a,b,c,d){Kb||Ib();var e=hd,f=Kb;Kb=!0;try{Hb(e,a,b,c,d)}finally{(Kb=f)||Mb()}}function id(a,b,c,d){ed(dd,hd.bind(null,a,b,c,d))}\nfunction hd(a,b,c,d){if(fd){var e;if((e=0===(b&4))&&0=be),ee=String.fromCharCode(32),fe=!1;\nfunction ge(a,b){switch(a){case \"keyup\":return-1!==$d.indexOf(b.keyCode);case \"keydown\":return 229!==b.keyCode;case \"keypress\":case \"mousedown\":case \"focusout\":return!0;default:return!1}}function he(a){a=a.detail;return\"object\"===typeof a&&\"data\"in a?a.data:null}var ie=!1;function je(a,b){switch(a){case \"compositionend\":return he(b);case \"keypress\":if(32!==b.which)return null;fe=!0;return ee;case \"textInput\":return a=b.data,a===ee&&fe?null:a;default:return null}}\nfunction ke(a,b){if(ie)return\"compositionend\"===a||!ae&&ge(a,b)?(a=nd(),md=ld=kd=null,ie=!1,a):null;switch(a){case \"paste\":return null;case \"keypress\":if(!(b.ctrlKey||b.altKey||b.metaKey)||b.ctrlKey&&b.altKey){if(b.char&&1=b)return{node:c,offset:b-a};a=d}a:{for(;c;){if(c.nextSibling){c=c.nextSibling;break a}c=c.parentNode}c=void 0}c=Ke(c)}}function Me(a,b){return a&&b?a===b?!0:a&&3===a.nodeType?!1:b&&3===b.nodeType?Me(a,b.parentNode):\"contains\"in a?a.contains(b):a.compareDocumentPosition?!!(a.compareDocumentPosition(b)&16):!1:!1}\nfunction Ne(){for(var a=window,b=Xa();b instanceof a.HTMLIFrameElement;){try{var c=\"string\"===typeof b.contentWindow.location.href}catch(d){c=!1}if(c)a=b.contentWindow;else break;b=Xa(a.document)}return b}function Oe(a){var b=a&&a.nodeName&&a.nodeName.toLowerCase();return b&&(\"input\"===b&&(\"text\"===a.type||\"search\"===a.type||\"tel\"===a.type||\"url\"===a.type||\"password\"===a.type)||\"textarea\"===b||\"true\"===a.contentEditable)}\nvar Pe=fa&&\"documentMode\"in document&&11>=document.documentMode,Qe=null,Re=null,Se=null,Te=!1;\nfunction Ue(a,b,c){var d=c.window===c?c.document:9===c.nodeType?c:c.ownerDocument;Te||null==Qe||Qe!==Xa(d)||(d=Qe,\"selectionStart\"in d&&Oe(d)?d={start:d.selectionStart,end:d.selectionEnd}:(d=(d.ownerDocument&&d.ownerDocument.defaultView||window).getSelection(),d={anchorNode:d.anchorNode,anchorOffset:d.anchorOffset,focusNode:d.focusNode,focusOffset:d.focusOffset}),Se&&Je(Se,d)||(Se=d,d=oe(Re,\"onSelect\"),0Af||(a.current=zf[Af],zf[Af]=null,Af--)}function I(a,b){Af++;zf[Af]=a.current;a.current=b}var Cf={},M=Bf(Cf),N=Bf(!1),Df=Cf;\nfunction Ef(a,b){var c=a.type.contextTypes;if(!c)return Cf;var d=a.stateNode;if(d&&d.__reactInternalMemoizedUnmaskedChildContext===b)return d.__reactInternalMemoizedMaskedChildContext;var e={},f;for(f in c)e[f]=b[f];d&&(a=a.stateNode,a.__reactInternalMemoizedUnmaskedChildContext=b,a.__reactInternalMemoizedMaskedChildContext=e);return e}function Ff(a){a=a.childContextTypes;return null!==a&&void 0!==a}function Gf(){H(N);H(M)}function Hf(a,b,c){if(M.current!==Cf)throw Error(y(168));I(M,b);I(N,c)}\nfunction If(a,b,c){var d=a.stateNode;a=b.childContextTypes;if(\"function\"!==typeof d.getChildContext)return c;d=d.getChildContext();for(var e in d)if(!(e in a))throw Error(y(108,Ra(b)||\"Unknown\",e));return m({},c,d)}function Jf(a){a=(a=a.stateNode)&&a.__reactInternalMemoizedMergedChildContext||Cf;Df=M.current;I(M,a);I(N,N.current);return!0}function Kf(a,b,c){var d=a.stateNode;if(!d)throw Error(y(169));c?(a=If(a,b,Df),d.__reactInternalMemoizedMergedChildContext=a,H(N),H(M),I(M,a)):H(N);I(N,c)}\nvar Lf=null,Mf=null,Nf=r.unstable_runWithPriority,Of=r.unstable_scheduleCallback,Pf=r.unstable_cancelCallback,Qf=r.unstable_shouldYield,Rf=r.unstable_requestPaint,Sf=r.unstable_now,Tf=r.unstable_getCurrentPriorityLevel,Uf=r.unstable_ImmediatePriority,Vf=r.unstable_UserBlockingPriority,Wf=r.unstable_NormalPriority,Xf=r.unstable_LowPriority,Yf=r.unstable_IdlePriority,Zf={},$f=void 0!==Rf?Rf:function(){},ag=null,bg=null,cg=!1,dg=Sf(),O=1E4>dg?Sf:function(){return Sf()-dg};\nfunction eg(){switch(Tf()){case Uf:return 99;case Vf:return 98;case Wf:return 97;case Xf:return 96;case Yf:return 95;default:throw Error(y(332));}}function fg(a){switch(a){case 99:return Uf;case 98:return Vf;case 97:return Wf;case 96:return Xf;case 95:return Yf;default:throw Error(y(332));}}function gg(a,b){a=fg(a);return Nf(a,b)}function hg(a,b,c){a=fg(a);return Of(a,b,c)}function ig(){if(null!==bg){var a=bg;bg=null;Pf(a)}jg()}\nfunction jg(){if(!cg&&null!==ag){cg=!0;var a=0;try{var b=ag;gg(99,function(){for(;az?(q=u,u=null):q=u.sibling;var n=p(e,u,h[z],k);if(null===n){null===u&&(u=q);break}a&&u&&null===\nn.alternate&&b(e,u);g=f(n,g,z);null===t?l=n:t.sibling=n;t=n;u=q}if(z===h.length)return c(e,u),l;if(null===u){for(;zz?(q=u,u=null):q=u.sibling;var w=p(e,u,n.value,k);if(null===w){null===u&&(u=q);break}a&&u&&null===w.alternate&&b(e,u);g=f(w,g,z);null===t?l=w:t.sibling=w;t=w;u=q}if(n.done)return c(e,u),l;if(null===u){for(;!n.done;z++,n=h.next())n=A(e,n.value,k),null!==n&&(g=f(n,g,z),null===t?l=n:t.sibling=n,t=n);return l}for(u=d(e,u);!n.done;z++,n=h.next())n=C(u,e,z,n.value,k),null!==n&&(a&&null!==n.alternate&&\nu.delete(null===n.key?z:n.key),g=f(n,g,z),null===t?l=n:t.sibling=n,t=n);a&&u.forEach(function(a){return b(e,a)});return l}return function(a,d,f,h){var k=\"object\"===typeof f&&null!==f&&f.type===ua&&null===f.key;k&&(f=f.props.children);var l=\"object\"===typeof f&&null!==f;if(l)switch(f.$$typeof){case sa:a:{l=f.key;for(k=d;null!==k;){if(k.key===l){switch(k.tag){case 7:if(f.type===ua){c(a,k.sibling);d=e(k,f.props.children);d.return=a;a=d;break a}break;default:if(k.elementType===f.type){c(a,k.sibling);\nd=e(k,f.props);d.ref=Qg(a,k,f);d.return=a;a=d;break a}}c(a,k);break}else b(a,k);k=k.sibling}f.type===ua?(d=Xg(f.props.children,a.mode,h,f.key),d.return=a,a=d):(h=Vg(f.type,f.key,f.props,null,a.mode,h),h.ref=Qg(a,d,f),h.return=a,a=h)}return g(a);case ta:a:{for(k=f.key;null!==d;){if(d.key===k)if(4===d.tag&&d.stateNode.containerInfo===f.containerInfo&&d.stateNode.implementation===f.implementation){c(a,d.sibling);d=e(d,f.children||[]);d.return=a;a=d;break a}else{c(a,d);break}else b(a,d);d=d.sibling}d=\nWg(f,a.mode,h);d.return=a;a=d}return g(a)}if(\"string\"===typeof f||\"number\"===typeof f)return f=\"\"+f,null!==d&&6===d.tag?(c(a,d.sibling),d=e(d,f),d.return=a,a=d):(c(a,d),d=Ug(f,a.mode,h),d.return=a,a=d),g(a);if(Pg(f))return x(a,d,f,h);if(La(f))return w(a,d,f,h);l&&Rg(a,f);if(\"undefined\"===typeof f&&!k)switch(a.tag){case 1:case 22:case 0:case 11:case 15:throw Error(y(152,Ra(a.type)||\"Component\"));}return c(a,d)}}var Yg=Sg(!0),Zg=Sg(!1),$g={},ah=Bf($g),bh=Bf($g),ch=Bf($g);\nfunction dh(a){if(a===$g)throw Error(y(174));return a}function eh(a,b){I(ch,b);I(bh,a);I(ah,$g);a=b.nodeType;switch(a){case 9:case 11:b=(b=b.documentElement)?b.namespaceURI:mb(null,\"\");break;default:a=8===a?b.parentNode:b,b=a.namespaceURI||null,a=a.tagName,b=mb(b,a)}H(ah);I(ah,b)}function fh(){H(ah);H(bh);H(ch)}function gh(a){dh(ch.current);var b=dh(ah.current);var c=mb(b,a.type);b!==c&&(I(bh,a),I(ah,c))}function hh(a){bh.current===a&&(H(ah),H(bh))}var P=Bf(0);\nfunction ih(a){for(var b=a;null!==b;){if(13===b.tag){var c=b.memoizedState;if(null!==c&&(c=c.dehydrated,null===c||\"$?\"===c.data||\"$!\"===c.data))return b}else if(19===b.tag&&void 0!==b.memoizedProps.revealOrder){if(0!==(b.flags&64))return b}else if(null!==b.child){b.child.return=b;b=b.child;continue}if(b===a)break;for(;null===b.sibling;){if(null===b.return||b.return===a)return null;b=b.return}b.sibling.return=b.return;b=b.sibling}return null}var jh=null,kh=null,lh=!1;\nfunction mh(a,b){var c=nh(5,null,null,0);c.elementType=\"DELETED\";c.type=\"DELETED\";c.stateNode=b;c.return=a;c.flags=8;null!==a.lastEffect?(a.lastEffect.nextEffect=c,a.lastEffect=c):a.firstEffect=a.lastEffect=c}function oh(a,b){switch(a.tag){case 5:var c=a.type;b=1!==b.nodeType||c.toLowerCase()!==b.nodeName.toLowerCase()?null:b;return null!==b?(a.stateNode=b,!0):!1;case 6:return b=\"\"===a.pendingProps||3!==b.nodeType?null:b,null!==b?(a.stateNode=b,!0):!1;case 13:return!1;default:return!1}}\nfunction ph(a){if(lh){var b=kh;if(b){var c=b;if(!oh(a,b)){b=rf(c.nextSibling);if(!b||!oh(a,b)){a.flags=a.flags&-1025|2;lh=!1;jh=a;return}mh(jh,c)}jh=a;kh=rf(b.firstChild)}else a.flags=a.flags&-1025|2,lh=!1,jh=a}}function qh(a){for(a=a.return;null!==a&&5!==a.tag&&3!==a.tag&&13!==a.tag;)a=a.return;jh=a}\nfunction rh(a){if(a!==jh)return!1;if(!lh)return qh(a),lh=!0,!1;var b=a.type;if(5!==a.tag||\"head\"!==b&&\"body\"!==b&&!nf(b,a.memoizedProps))for(b=kh;b;)mh(a,b),b=rf(b.nextSibling);qh(a);if(13===a.tag){a=a.memoizedState;a=null!==a?a.dehydrated:null;if(!a)throw Error(y(317));a:{a=a.nextSibling;for(b=0;a;){if(8===a.nodeType){var c=a.data;if(\"/$\"===c){if(0===b){kh=rf(a.nextSibling);break a}b--}else\"$\"!==c&&\"$!\"!==c&&\"$?\"!==c||b++}a=a.nextSibling}kh=null}}else kh=jh?rf(a.stateNode.nextSibling):null;return!0}\nfunction sh(){kh=jh=null;lh=!1}var th=[];function uh(){for(var a=0;af))throw Error(y(301));f+=1;T=S=null;b.updateQueue=null;vh.current=Fh;a=c(d,e)}while(zh)}vh.current=Gh;b=null!==S&&null!==S.next;xh=0;T=S=R=null;yh=!1;if(b)throw Error(y(300));return a}function Hh(){var a={memoizedState:null,baseState:null,baseQueue:null,queue:null,next:null};null===T?R.memoizedState=T=a:T=T.next=a;return T}\nfunction Ih(){if(null===S){var a=R.alternate;a=null!==a?a.memoizedState:null}else a=S.next;var b=null===T?R.memoizedState:T.next;if(null!==b)T=b,S=a;else{if(null===a)throw Error(y(310));S=a;a={memoizedState:S.memoizedState,baseState:S.baseState,baseQueue:S.baseQueue,queue:S.queue,next:null};null===T?R.memoizedState=T=a:T=T.next=a}return T}function Jh(a,b){return\"function\"===typeof b?b(a):b}\nfunction Kh(a){var b=Ih(),c=b.queue;if(null===c)throw Error(y(311));c.lastRenderedReducer=a;var d=S,e=d.baseQueue,f=c.pending;if(null!==f){if(null!==e){var g=e.next;e.next=f.next;f.next=g}d.baseQueue=e=f;c.pending=null}if(null!==e){e=e.next;d=d.baseState;var h=g=f=null,k=e;do{var l=k.lane;if((xh&l)===l)null!==h&&(h=h.next={lane:0,action:k.action,eagerReducer:k.eagerReducer,eagerState:k.eagerState,next:null}),d=k.eagerReducer===a?k.eagerState:a(d,k.action);else{var n={lane:l,action:k.action,eagerReducer:k.eagerReducer,\neagerState:k.eagerState,next:null};null===h?(g=h=n,f=d):h=h.next=n;R.lanes|=l;Dg|=l}k=k.next}while(null!==k&&k!==e);null===h?f=d:h.next=g;He(d,b.memoizedState)||(ug=!0);b.memoizedState=d;b.baseState=f;b.baseQueue=h;c.lastRenderedState=d}return[b.memoizedState,c.dispatch]}\nfunction Lh(a){var b=Ih(),c=b.queue;if(null===c)throw Error(y(311));c.lastRenderedReducer=a;var d=c.dispatch,e=c.pending,f=b.memoizedState;if(null!==e){c.pending=null;var g=e=e.next;do f=a(f,g.action),g=g.next;while(g!==e);He(f,b.memoizedState)||(ug=!0);b.memoizedState=f;null===b.baseQueue&&(b.baseState=f);c.lastRenderedState=f}return[f,d]}\nfunction Mh(a,b,c){var d=b._getVersion;d=d(b._source);var e=b._workInProgressVersionPrimary;if(null!==e)a=e===d;else if(a=a.mutableReadLanes,a=(xh&a)===a)b._workInProgressVersionPrimary=d,th.push(b);if(a)return c(b._source);th.push(b);throw Error(y(350));}\nfunction Nh(a,b,c,d){var e=U;if(null===e)throw Error(y(349));var f=b._getVersion,g=f(b._source),h=vh.current,k=h.useState(function(){return Mh(e,b,c)}),l=k[1],n=k[0];k=T;var A=a.memoizedState,p=A.refs,C=p.getSnapshot,x=A.source;A=A.subscribe;var w=R;a.memoizedState={refs:p,source:b,subscribe:d};h.useEffect(function(){p.getSnapshot=c;p.setSnapshot=l;var a=f(b._source);if(!He(g,a)){a=c(b._source);He(n,a)||(l(a),a=Ig(w),e.mutableReadLanes|=a&e.pendingLanes);a=e.mutableReadLanes;e.entangledLanes|=a;for(var d=\ne.entanglements,h=a;0c?98:c,function(){a(!0)});gg(97\\x3c/script>\",a=a.removeChild(a.firstChild)):\"string\"===typeof d.is?a=g.createElement(c,{is:d.is}):(a=g.createElement(c),\"select\"===c&&(g=a,d.multiple?g.multiple=!0:d.size&&(g.size=d.size))):a=g.createElementNS(a,c);a[wf]=b;a[xf]=d;Bi(a,b,!1,!1);b.stateNode=a;g=wb(c,d);switch(c){case \"dialog\":G(\"cancel\",a);G(\"close\",a);\ne=d;break;case \"iframe\":case \"object\":case \"embed\":G(\"load\",a);e=d;break;case \"video\":case \"audio\":for(e=0;eJi&&(b.flags|=64,f=!0,Fi(d,!1),b.lanes=33554432)}else{if(!f)if(a=ih(g),null!==a){if(b.flags|=64,f=!0,c=a.updateQueue,null!==c&&(b.updateQueue=c,b.flags|=4),Fi(d,!0),null===d.tail&&\"hidden\"===d.tailMode&&!g.alternate&&!lh)return b=b.lastEffect=d.lastEffect,null!==b&&(b.nextEffect=null),null}else 2*O()-d.renderingStartTime>Ji&&1073741824!==c&&(b.flags|=\n64,f=!0,Fi(d,!1),b.lanes=33554432);d.isBackwards?(g.sibling=b.child,b.child=g):(c=d.last,null!==c?c.sibling=g:b.child=g,d.last=g)}return null!==d.tail?(c=d.tail,d.rendering=c,d.tail=c.sibling,d.lastEffect=b.lastEffect,d.renderingStartTime=O(),c.sibling=null,b=P.current,I(P,f?b&1|2:b&1),c):null;case 23:case 24:return Ki(),null!==a&&null!==a.memoizedState!==(null!==b.memoizedState)&&\"unstable-defer-without-hiding\"!==d.mode&&(b.flags|=4),null}throw Error(y(156,b.tag));}\nfunction Li(a){switch(a.tag){case 1:Ff(a.type)&&Gf();var b=a.flags;return b&4096?(a.flags=b&-4097|64,a):null;case 3:fh();H(N);H(M);uh();b=a.flags;if(0!==(b&64))throw Error(y(285));a.flags=b&-4097|64;return a;case 5:return hh(a),null;case 13:return H(P),b=a.flags,b&4096?(a.flags=b&-4097|64,a):null;case 19:return H(P),null;case 4:return fh(),null;case 10:return rg(a),null;case 23:case 24:return Ki(),null;default:return null}}\nfunction Mi(a,b){try{var c=\"\",d=b;do c+=Qa(d),d=d.return;while(d);var e=c}catch(f){e=\"\\nError generating stack: \"+f.message+\"\\n\"+f.stack}return{value:a,source:b,stack:e}}function Ni(a,b){try{console.error(b.value)}catch(c){setTimeout(function(){throw c;})}}var Oi=\"function\"===typeof WeakMap?WeakMap:Map;function Pi(a,b,c){c=zg(-1,c);c.tag=3;c.payload={element:null};var d=b.value;c.callback=function(){Qi||(Qi=!0,Ri=d);Ni(a,b)};return c}\nfunction Si(a,b,c){c=zg(-1,c);c.tag=3;var d=a.type.getDerivedStateFromError;if(\"function\"===typeof d){var e=b.value;c.payload=function(){Ni(a,b);return d(e)}}var f=a.stateNode;null!==f&&\"function\"===typeof f.componentDidCatch&&(c.callback=function(){\"function\"!==typeof d&&(null===Ti?Ti=new Set([this]):Ti.add(this),Ni(a,b));var c=b.stack;this.componentDidCatch(b.value,{componentStack:null!==c?c:\"\"})});return c}var Ui=\"function\"===typeof WeakSet?WeakSet:Set;\nfunction Vi(a){var b=a.ref;if(null!==b)if(\"function\"===typeof b)try{b(null)}catch(c){Wi(a,c)}else b.current=null}function Xi(a,b){switch(b.tag){case 0:case 11:case 15:case 22:return;case 1:if(b.flags&256&&null!==a){var c=a.memoizedProps,d=a.memoizedState;a=b.stateNode;b=a.getSnapshotBeforeUpdate(b.elementType===b.type?c:lg(b.type,c),d);a.__reactInternalSnapshotBeforeUpdate=b}return;case 3:b.flags&256&&qf(b.stateNode.containerInfo);return;case 5:case 6:case 4:case 17:return}throw Error(y(163));}\nfunction Yi(a,b,c){switch(c.tag){case 0:case 11:case 15:case 22:b=c.updateQueue;b=null!==b?b.lastEffect:null;if(null!==b){a=b=b.next;do{if(3===(a.tag&3)){var d=a.create;a.destroy=d()}a=a.next}while(a!==b)}b=c.updateQueue;b=null!==b?b.lastEffect:null;if(null!==b){a=b=b.next;do{var e=a;d=e.next;e=e.tag;0!==(e&4)&&0!==(e&1)&&(Zi(c,a),$i(c,a));a=d}while(a!==b)}return;case 1:a=c.stateNode;c.flags&4&&(null===b?a.componentDidMount():(d=c.elementType===c.type?b.memoizedProps:lg(c.type,b.memoizedProps),a.componentDidUpdate(d,\nb.memoizedState,a.__reactInternalSnapshotBeforeUpdate)));b=c.updateQueue;null!==b&&Eg(c,b,a);return;case 3:b=c.updateQueue;if(null!==b){a=null;if(null!==c.child)switch(c.child.tag){case 5:a=c.child.stateNode;break;case 1:a=c.child.stateNode}Eg(c,b,a)}return;case 5:a=c.stateNode;null===b&&c.flags&4&&mf(c.type,c.memoizedProps)&&a.focus();return;case 6:return;case 4:return;case 12:return;case 13:null===c.memoizedState&&(c=c.alternate,null!==c&&(c=c.memoizedState,null!==c&&(c=c.dehydrated,null!==c&&Cc(c))));\nreturn;case 19:case 17:case 20:case 21:case 23:case 24:return}throw Error(y(163));}\nfunction aj(a,b){for(var c=a;;){if(5===c.tag){var d=c.stateNode;if(b)d=d.style,\"function\"===typeof d.setProperty?d.setProperty(\"display\",\"none\",\"important\"):d.display=\"none\";else{d=c.stateNode;var e=c.memoizedProps.style;e=void 0!==e&&null!==e&&e.hasOwnProperty(\"display\")?e.display:null;d.style.display=sb(\"display\",e)}}else if(6===c.tag)c.stateNode.nodeValue=b?\"\":c.memoizedProps;else if((23!==c.tag&&24!==c.tag||null===c.memoizedState||c===a)&&null!==c.child){c.child.return=c;c=c.child;continue}if(c===\na)break;for(;null===c.sibling;){if(null===c.return||c.return===a)return;c=c.return}c.sibling.return=c.return;c=c.sibling}}\nfunction bj(a,b){if(Mf&&\"function\"===typeof Mf.onCommitFiberUnmount)try{Mf.onCommitFiberUnmount(Lf,b)}catch(f){}switch(b.tag){case 0:case 11:case 14:case 15:case 22:a=b.updateQueue;if(null!==a&&(a=a.lastEffect,null!==a)){var c=a=a.next;do{var d=c,e=d.destroy;d=d.tag;if(void 0!==e)if(0!==(d&4))Zi(b,c);else{d=b;try{e()}catch(f){Wi(d,f)}}c=c.next}while(c!==a)}break;case 1:Vi(b);a=b.stateNode;if(\"function\"===typeof a.componentWillUnmount)try{a.props=b.memoizedProps,a.state=b.memoizedState,a.componentWillUnmount()}catch(f){Wi(b,\nf)}break;case 5:Vi(b);break;case 4:cj(a,b)}}function dj(a){a.alternate=null;a.child=null;a.dependencies=null;a.firstEffect=null;a.lastEffect=null;a.memoizedProps=null;a.memoizedState=null;a.pendingProps=null;a.return=null;a.updateQueue=null}function ej(a){return 5===a.tag||3===a.tag||4===a.tag}\nfunction fj(a){a:{for(var b=a.return;null!==b;){if(ej(b))break a;b=b.return}throw Error(y(160));}var c=b;b=c.stateNode;switch(c.tag){case 5:var d=!1;break;case 3:b=b.containerInfo;d=!0;break;case 4:b=b.containerInfo;d=!0;break;default:throw Error(y(161));}c.flags&16&&(pb(b,\"\"),c.flags&=-17);a:b:for(c=a;;){for(;null===c.sibling;){if(null===c.return||ej(c.return)){c=null;break a}c=c.return}c.sibling.return=c.return;for(c=c.sibling;5!==c.tag&&6!==c.tag&&18!==c.tag;){if(c.flags&2)continue b;if(null===\nc.child||4===c.tag)continue b;else c.child.return=c,c=c.child}if(!(c.flags&2)){c=c.stateNode;break a}}d?gj(a,c,b):hj(a,c,b)}\nfunction gj(a,b,c){var d=a.tag,e=5===d||6===d;if(e)a=e?a.stateNode:a.stateNode.instance,b?8===c.nodeType?c.parentNode.insertBefore(a,b):c.insertBefore(a,b):(8===c.nodeType?(b=c.parentNode,b.insertBefore(a,c)):(b=c,b.appendChild(a)),c=c._reactRootContainer,null!==c&&void 0!==c||null!==b.onclick||(b.onclick=jf));else if(4!==d&&(a=a.child,null!==a))for(gj(a,b,c),a=a.sibling;null!==a;)gj(a,b,c),a=a.sibling}\nfunction hj(a,b,c){var d=a.tag,e=5===d||6===d;if(e)a=e?a.stateNode:a.stateNode.instance,b?c.insertBefore(a,b):c.appendChild(a);else if(4!==d&&(a=a.child,null!==a))for(hj(a,b,c),a=a.sibling;null!==a;)hj(a,b,c),a=a.sibling}\nfunction cj(a,b){for(var c=b,d=!1,e,f;;){if(!d){d=c.return;a:for(;;){if(null===d)throw Error(y(160));e=d.stateNode;switch(d.tag){case 5:f=!1;break a;case 3:e=e.containerInfo;f=!0;break a;case 4:e=e.containerInfo;f=!0;break a}d=d.return}d=!0}if(5===c.tag||6===c.tag){a:for(var g=a,h=c,k=h;;)if(bj(g,k),null!==k.child&&4!==k.tag)k.child.return=k,k=k.child;else{if(k===h)break a;for(;null===k.sibling;){if(null===k.return||k.return===h)break a;k=k.return}k.sibling.return=k.return;k=k.sibling}f?(g=e,h=c.stateNode,\n8===g.nodeType?g.parentNode.removeChild(h):g.removeChild(h)):e.removeChild(c.stateNode)}else if(4===c.tag){if(null!==c.child){e=c.stateNode.containerInfo;f=!0;c.child.return=c;c=c.child;continue}}else if(bj(a,c),null!==c.child){c.child.return=c;c=c.child;continue}if(c===b)break;for(;null===c.sibling;){if(null===c.return||c.return===b)return;c=c.return;4===c.tag&&(d=!1)}c.sibling.return=c.return;c=c.sibling}}\nfunction ij(a,b){switch(b.tag){case 0:case 11:case 14:case 15:case 22:var c=b.updateQueue;c=null!==c?c.lastEffect:null;if(null!==c){var d=c=c.next;do 3===(d.tag&3)&&(a=d.destroy,d.destroy=void 0,void 0!==a&&a()),d=d.next;while(d!==c)}return;case 1:return;case 5:c=b.stateNode;if(null!=c){d=b.memoizedProps;var e=null!==a?a.memoizedProps:d;a=b.type;var f=b.updateQueue;b.updateQueue=null;if(null!==f){c[xf]=d;\"input\"===a&&\"radio\"===d.type&&null!=d.name&&$a(c,d);wb(a,e);b=wb(a,d);for(e=0;ee&&(e=g);c&=~f}c=e;c=O()-c;c=(120>c?120:480>c?480:1080>c?1080:1920>c?1920:3E3>c?3E3:4320>\nc?4320:1960*nj(c/1960))-c;if(10 component higher in the tree to provide a loading indicator or placeholder to display.\")}5!==V&&(V=2);k=Mi(k,h);p=\ng;do{switch(p.tag){case 3:f=k;p.flags|=4096;b&=-b;p.lanes|=b;var J=Pi(p,f,b);Bg(p,J);break a;case 1:f=k;var K=p.type,Q=p.stateNode;if(0===(p.flags&64)&&(\"function\"===typeof K.getDerivedStateFromError||null!==Q&&\"function\"===typeof Q.componentDidCatch&&(null===Ti||!Ti.has(Q)))){p.flags|=4096;b&=-b;p.lanes|=b;var L=Si(p,f,b);Bg(p,L);break a}}p=p.return}while(null!==p)}Zj(c)}catch(va){b=va;Y===c&&null!==c&&(Y=c=c.return);continue}break}while(1)}\nfunction Pj(){var a=oj.current;oj.current=Gh;return null===a?Gh:a}function Tj(a,b){var c=X;X|=16;var d=Pj();U===a&&W===b||Qj(a,b);do try{ak();break}catch(e){Sj(a,e)}while(1);qg();X=c;oj.current=d;if(null!==Y)throw Error(y(261));U=null;W=0;return V}function ak(){for(;null!==Y;)bk(Y)}function Rj(){for(;null!==Y&&!Qf();)bk(Y)}function bk(a){var b=ck(a.alternate,a,qj);a.memoizedProps=a.pendingProps;null===b?Zj(a):Y=b;pj.current=null}\nfunction Zj(a){var b=a;do{var c=b.alternate;a=b.return;if(0===(b.flags&2048)){c=Gi(c,b,qj);if(null!==c){Y=c;return}c=b;if(24!==c.tag&&23!==c.tag||null===c.memoizedState||0!==(qj&1073741824)||0===(c.mode&4)){for(var d=0,e=c.child;null!==e;)d|=e.lanes|e.childLanes,e=e.sibling;c.childLanes=d}null!==a&&0===(a.flags&2048)&&(null===a.firstEffect&&(a.firstEffect=b.firstEffect),null!==b.lastEffect&&(null!==a.lastEffect&&(a.lastEffect.nextEffect=b.firstEffect),a.lastEffect=b.lastEffect),1g&&(h=g,g=J,J=h),h=Le(t,J),f=Le(t,g),h&&f&&(1!==v.rangeCount||v.anchorNode!==h.node||v.anchorOffset!==h.offset||v.focusNode!==f.node||v.focusOffset!==f.offset)&&(q=q.createRange(),q.setStart(h.node,h.offset),v.removeAllRanges(),J>g?(v.addRange(q),v.extend(f.node,f.offset)):(q.setEnd(f.node,f.offset),v.addRange(q))))));q=[];for(v=t;v=v.parentNode;)1===v.nodeType&&q.push({element:v,left:v.scrollLeft,top:v.scrollTop});\"function\"===typeof t.focus&&t.focus();for(t=\n0;tO()-jj?Qj(a,0):uj|=c);Mj(a,b)}function lj(a,b){var c=a.stateNode;null!==c&&c.delete(b);b=0;0===b&&(b=a.mode,0===(b&2)?b=1:0===(b&4)?b=99===eg()?1:2:(0===Gj&&(Gj=tj),b=Yc(62914560&~Gj),0===b&&(b=4194304)));c=Hg();a=Kj(a,b);null!==a&&($c(a,b,c),Mj(a,c))}var ck;\nck=function(a,b,c){var d=b.lanes;if(null!==a)if(a.memoizedProps!==b.pendingProps||N.current)ug=!0;else if(0!==(c&d))ug=0!==(a.flags&16384)?!0:!1;else{ug=!1;switch(b.tag){case 3:ri(b);sh();break;case 5:gh(b);break;case 1:Ff(b.type)&&Jf(b);break;case 4:eh(b,b.stateNode.containerInfo);break;case 10:d=b.memoizedProps.value;var e=b.type._context;I(mg,e._currentValue);e._currentValue=d;break;case 13:if(null!==b.memoizedState){if(0!==(c&b.child.childLanes))return ti(a,b,c);I(P,P.current&1);b=hi(a,b,c);return null!==\nb?b.sibling:null}I(P,P.current&1);break;case 19:d=0!==(c&b.childLanes);if(0!==(a.flags&64)){if(d)return Ai(a,b,c);b.flags|=64}e=b.memoizedState;null!==e&&(e.rendering=null,e.tail=null,e.lastEffect=null);I(P,P.current);if(d)break;else return null;case 23:case 24:return b.lanes=0,mi(a,b,c)}return hi(a,b,c)}else ug=!1;b.lanes=0;switch(b.tag){case 2:d=b.type;null!==a&&(a.alternate=null,b.alternate=null,b.flags|=2);a=b.pendingProps;e=Ef(b,M.current);tg(b,c);e=Ch(null,b,d,a,e,c);b.flags|=1;if(\"object\"===\ntypeof e&&null!==e&&\"function\"===typeof e.render&&void 0===e.$$typeof){b.tag=1;b.memoizedState=null;b.updateQueue=null;if(Ff(d)){var f=!0;Jf(b)}else f=!1;b.memoizedState=null!==e.state&&void 0!==e.state?e.state:null;xg(b);var g=d.getDerivedStateFromProps;\"function\"===typeof g&&Gg(b,d,g,a);e.updater=Kg;b.stateNode=e;e._reactInternals=b;Og(b,d,a,c);b=qi(null,b,d,!0,f,c)}else b.tag=0,fi(null,b,e,c),b=b.child;return b;case 16:e=b.elementType;a:{null!==a&&(a.alternate=null,b.alternate=null,b.flags|=2);\na=b.pendingProps;f=e._init;e=f(e._payload);b.type=e;f=b.tag=hk(e);a=lg(e,a);switch(f){case 0:b=li(null,b,e,a,c);break a;case 1:b=pi(null,b,e,a,c);break a;case 11:b=gi(null,b,e,a,c);break a;case 14:b=ii(null,b,e,lg(e.type,a),d,c);break a}throw Error(y(306,e,\"\"));}return b;case 0:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:lg(d,e),li(a,b,d,e,c);case 1:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:lg(d,e),pi(a,b,d,e,c);case 3:ri(b);d=b.updateQueue;if(null===a||null===d)throw Error(y(282));\nd=b.pendingProps;e=b.memoizedState;e=null!==e?e.element:null;yg(a,b);Cg(b,d,null,c);d=b.memoizedState.element;if(d===e)sh(),b=hi(a,b,c);else{e=b.stateNode;if(f=e.hydrate)kh=rf(b.stateNode.containerInfo.firstChild),jh=b,f=lh=!0;if(f){a=e.mutableSourceEagerHydrationData;if(null!=a)for(e=0;e=\nE};k=function(){};exports.unstable_forceFrameRate=function(a){0>a||125>>1,e=a[d];if(void 0!==e&&0I(n,c))void 0!==r&&0>I(r,n)?(a[d]=r,a[v]=c,d=v):(a[d]=n,a[m]=c,d=m);else if(void 0!==r&&0>I(r,c))a[d]=r,a[v]=c,d=v;else break a}}return b}return null}function I(a,b){var c=a.sortIndex-b.sortIndex;return 0!==c?c:a.id-b.id}var L=[],M=[],N=1,O=null,P=3,Q=!1,R=!1,S=!1;\nfunction T(a){for(var b=J(M);null!==b;){if(null===b.callback)K(M);else if(b.startTime<=a)K(M),b.sortIndex=b.expirationTime,H(L,b);else break;b=J(M)}}function U(a){S=!1;T(a);if(!R)if(null!==J(L))R=!0,f(V);else{var b=J(M);null!==b&&g(U,b.startTime-a)}}\nfunction V(a,b){R=!1;S&&(S=!1,h());Q=!0;var c=P;try{T(b);for(O=J(L);null!==O&&(!(O.expirationTime>b)||a&&!exports.unstable_shouldYield());){var d=O.callback;if(\"function\"===typeof d){O.callback=null;P=O.priorityLevel;var e=d(O.expirationTime<=b);b=exports.unstable_now();\"function\"===typeof e?O.callback=e:O===J(L)&&K(L);T(b)}else K(L);O=J(L)}if(null!==O)var m=!0;else{var n=J(M);null!==n&&g(U,n.startTime-b);m=!1}return m}finally{O=null,P=c,Q=!1}}var W=k;exports.unstable_IdlePriority=5;\nexports.unstable_ImmediatePriority=1;exports.unstable_LowPriority=4;exports.unstable_NormalPriority=3;exports.unstable_Profiling=null;exports.unstable_UserBlockingPriority=2;exports.unstable_cancelCallback=function(a){a.callback=null};exports.unstable_continueExecution=function(){R||Q||(R=!0,f(V))};exports.unstable_getCurrentPriorityLevel=function(){return P};exports.unstable_getFirstCallbackNode=function(){return J(L)};\nexports.unstable_next=function(a){switch(P){case 1:case 2:case 3:var b=3;break;default:b=P}var c=P;P=b;try{return a()}finally{P=c}};exports.unstable_pauseExecution=function(){};exports.unstable_requestPaint=W;exports.unstable_runWithPriority=function(a,b){switch(a){case 1:case 2:case 3:case 4:case 5:break;default:a=3}var c=P;P=a;try{return b()}finally{P=c}};\nexports.unstable_scheduleCallback=function(a,b,c){var d=exports.unstable_now();\"object\"===typeof c&&null!==c?(c=c.delay,c=\"number\"===typeof c&&0d?(a.sortIndex=c,H(M,a),null===J(L)&&a===J(M)&&(S?h():S=!0,g(U,c-d))):(a.sortIndex=e,H(L,a),R||Q||(R=!0,f(V)));return a};\nexports.unstable_wrapCallback=function(a){var b=P;return function(){var c=P;P=b;try{return a.apply(this,arguments)}finally{P=c}}};\n","// .dirname, .basename, and .extname methods are extracted from Node.js v8.11.1,\n// backported and transplited with Babel, with backwards-compat fixes\n\n// Copyright Joyent, Inc. and other Node contributors.\n//\n// Permission is hereby granted, free of charge, to any person obtaining a\n// copy of this software and associated documentation files (the\n// \"Software\"), to deal in the Software without restriction, including\n// without limitation the rights to use, copy, modify, merge, publish,\n// distribute, sublicense, and/or sell copies of the Software, and to permit\n// persons to whom the Software is furnished to do so, subject to the\n// following conditions:\n//\n// The above copyright notice and this permission notice shall be included\n// in all copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS\n// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN\n// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,\n// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\n// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE\n// USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n// resolves . and .. elements in a path array with directory names there\n// must be no slashes, empty elements, or device names (c:\\) in the array\n// (so also no leading and trailing slashes - it does not distinguish\n// relative and absolute paths)\nfunction normalizeArray(parts, allowAboveRoot) {\n // if the path tries to go above the root, `up` ends up > 0\n var up = 0;\n for (var i = parts.length - 1; i >= 0; i--) {\n var last = parts[i];\n if (last === '.') {\n parts.splice(i, 1);\n } else if (last === '..') {\n parts.splice(i, 1);\n up++;\n } else if (up) {\n parts.splice(i, 1);\n up--;\n }\n }\n\n // if the path is allowed to go above the root, restore leading ..s\n if (allowAboveRoot) {\n for (; up--; up) {\n parts.unshift('..');\n }\n }\n\n return parts;\n}\n\n// path.resolve([from ...], to)\n// posix version\nexports.resolve = function() {\n var resolvedPath = '',\n resolvedAbsolute = false;\n\n for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) {\n var path = (i >= 0) ? arguments[i] : process.cwd();\n\n // Skip empty and invalid entries\n if (typeof path !== 'string') {\n throw new TypeError('Arguments to path.resolve must be strings');\n } else if (!path) {\n continue;\n }\n\n resolvedPath = path + '/' + resolvedPath;\n resolvedAbsolute = path.charAt(0) === '/';\n }\n\n // At this point the path should be resolved to a full absolute path, but\n // handle relative paths to be safe (might happen when process.cwd() fails)\n\n // Normalize the path\n resolvedPath = normalizeArray(filter(resolvedPath.split('/'), function(p) {\n return !!p;\n }), !resolvedAbsolute).join('/');\n\n return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.';\n};\n\n// path.normalize(path)\n// posix version\nexports.normalize = function(path) {\n var isAbsolute = exports.isAbsolute(path),\n trailingSlash = substr(path, -1) === '/';\n\n // Normalize the path\n path = normalizeArray(filter(path.split('/'), function(p) {\n return !!p;\n }), !isAbsolute).join('/');\n\n if (!path && !isAbsolute) {\n path = '.';\n }\n if (path && trailingSlash) {\n path += '/';\n }\n\n return (isAbsolute ? '/' : '') + path;\n};\n\n// posix version\nexports.isAbsolute = function(path) {\n return path.charAt(0) === '/';\n};\n\n// posix version\nexports.join = function() {\n var paths = Array.prototype.slice.call(arguments, 0);\n return exports.normalize(filter(paths, function(p, index) {\n if (typeof p !== 'string') {\n throw new TypeError('Arguments to path.join must be strings');\n }\n return p;\n }).join('/'));\n};\n\n\n// path.relative(from, to)\n// posix version\nexports.relative = function(from, to) {\n from = exports.resolve(from).substr(1);\n to = exports.resolve(to).substr(1);\n\n function trim(arr) {\n var start = 0;\n for (; start < arr.length; start++) {\n if (arr[start] !== '') break;\n }\n\n var end = arr.length - 1;\n for (; end >= 0; end--) {\n if (arr[end] !== '') break;\n }\n\n if (start > end) return [];\n return arr.slice(start, end - start + 1);\n }\n\n var fromParts = trim(from.split('/'));\n var toParts = trim(to.split('/'));\n\n var length = Math.min(fromParts.length, toParts.length);\n var samePartsLength = length;\n for (var i = 0; i < length; i++) {\n if (fromParts[i] !== toParts[i]) {\n samePartsLength = i;\n break;\n }\n }\n\n var outputParts = [];\n for (var i = samePartsLength; i < fromParts.length; i++) {\n outputParts.push('..');\n }\n\n outputParts = outputParts.concat(toParts.slice(samePartsLength));\n\n return outputParts.join('/');\n};\n\nexports.sep = '/';\nexports.delimiter = ':';\n\nexports.dirname = function (path) {\n if (typeof path !== 'string') path = path + '';\n if (path.length === 0) return '.';\n var code = path.charCodeAt(0);\n var hasRoot = code === 47 /*/*/;\n var end = -1;\n var matchedSlash = true;\n for (var i = path.length - 1; i >= 1; --i) {\n code = path.charCodeAt(i);\n if (code === 47 /*/*/) {\n if (!matchedSlash) {\n end = i;\n break;\n }\n } else {\n // We saw the first non-path separator\n matchedSlash = false;\n }\n }\n\n if (end === -1) return hasRoot ? '/' : '.';\n if (hasRoot && end === 1) {\n // return '//';\n // Backwards-compat fix:\n return '/';\n }\n return path.slice(0, end);\n};\n\nfunction basename(path) {\n if (typeof path !== 'string') path = path + '';\n\n var start = 0;\n var end = -1;\n var matchedSlash = true;\n var i;\n\n for (i = path.length - 1; i >= 0; --i) {\n if (path.charCodeAt(i) === 47 /*/*/) {\n // If we reached a path separator that was not part of a set of path\n // separators at the end of the string, stop now\n if (!matchedSlash) {\n start = i + 1;\n break;\n }\n } else if (end === -1) {\n // We saw the first non-path separator, mark this as the end of our\n // path component\n matchedSlash = false;\n end = i + 1;\n }\n }\n\n if (end === -1) return '';\n return path.slice(start, end);\n}\n\n// Uses a mixed approach for backwards-compatibility, as ext behavior changed\n// in new Node.js versions, so only basename() above is backported here\nexports.basename = function (path, ext) {\n var f = basename(path);\n if (ext && f.substr(-1 * ext.length) === ext) {\n f = f.substr(0, f.length - ext.length);\n }\n return f;\n};\n\nexports.extname = function (path) {\n if (typeof path !== 'string') path = path + '';\n var startDot = -1;\n var startPart = 0;\n var end = -1;\n var matchedSlash = true;\n // Track the state of characters (if any) we see before our first dot and\n // after any path separator we find\n var preDotState = 0;\n for (var i = path.length - 1; i >= 0; --i) {\n var code = path.charCodeAt(i);\n if (code === 47 /*/*/) {\n // If we reached a path separator that was not part of a set of path\n // separators at the end of the string, stop now\n if (!matchedSlash) {\n startPart = i + 1;\n break;\n }\n continue;\n }\n if (end === -1) {\n // We saw the first non-path separator, mark this as the end of our\n // extension\n matchedSlash = false;\n end = i + 1;\n }\n if (code === 46 /*.*/) {\n // If this is our first dot, mark it as the start of our extension\n if (startDot === -1)\n startDot = i;\n else if (preDotState !== 1)\n preDotState = 1;\n } else if (startDot !== -1) {\n // We saw a non-dot and non-path separator before our dot, so we should\n // have a good chance at having a non-empty extension\n preDotState = -1;\n }\n }\n\n if (startDot === -1 || end === -1 ||\n // We saw a non-dot character immediately before the dot\n preDotState === 0 ||\n // The (right-most) trimmed path component is exactly '..'\n preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) {\n return '';\n }\n return path.slice(startDot, end);\n};\n\nfunction filter (xs, f) {\n if (xs.filter) return xs.filter(f);\n var res = [];\n for (var i = 0; i < xs.length; i++) {\n if (f(xs[i], i, xs)) res.push(xs[i]);\n }\n return res;\n}\n\n// String.prototype.substr - negative index don't work in IE8\nvar substr = 'ab'.substr(-1) === 'b'\n ? function (str, start, len) { return str.substr(start, len) }\n : function (str, start, len) {\n if (start < 0) start = str.length + start;\n return str.substr(start, len);\n }\n;\n","exports.endianness = function () { return 'LE' };\n\nexports.hostname = function () {\n if (typeof location !== 'undefined') {\n return location.hostname\n }\n else return '';\n};\n\nexports.loadavg = function () { return [] };\n\nexports.uptime = function () { return 0 };\n\nexports.freemem = function () {\n return Number.MAX_VALUE;\n};\n\nexports.totalmem = function () {\n return Number.MAX_VALUE;\n};\n\nexports.cpus = function () { return [] };\n\nexports.type = function () { return 'Browser' };\n\nexports.release = function () {\n if (typeof navigator !== 'undefined') {\n return navigator.appVersion;\n }\n return '';\n};\n\nexports.networkInterfaces\n= exports.getNetworkInterfaces\n= function () { return {} };\n\nexports.arch = function () { return 'javascript' };\n\nexports.platform = function () { return 'browser' };\n\nexports.tmpdir = exports.tmpDir = function () {\n return '/tmp';\n};\n\nexports.EOL = '\\n';\n\nexports.homedir = function () {\n\treturn '/'\n};\n","/**\n * Copyright (c) 2014-present, Facebook, Inc.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nvar runtime = (function (exports) {\n \"use strict\";\n\n var Op = Object.prototype;\n var hasOwn = Op.hasOwnProperty;\n var undefined; // More compressible than void 0.\n var $Symbol = typeof Symbol === \"function\" ? Symbol : {};\n var iteratorSymbol = $Symbol.iterator || \"@@iterator\";\n var asyncIteratorSymbol = $Symbol.asyncIterator || \"@@asyncIterator\";\n var toStringTagSymbol = $Symbol.toStringTag || \"@@toStringTag\";\n\n function define(obj, key, value) {\n Object.defineProperty(obj, key, {\n value: value,\n enumerable: true,\n configurable: true,\n writable: true\n });\n return obj[key];\n }\n try {\n // IE 8 has a broken Object.defineProperty that only works on DOM objects.\n define({}, \"\");\n } catch (err) {\n define = function(obj, key, value) {\n return obj[key] = value;\n };\n }\n\n function wrap(innerFn, outerFn, self, tryLocsList) {\n // If outerFn provided and outerFn.prototype is a Generator, then outerFn.prototype instanceof Generator.\n var protoGenerator = outerFn && outerFn.prototype instanceof Generator ? outerFn : Generator;\n var generator = Object.create(protoGenerator.prototype);\n var context = new Context(tryLocsList || []);\n\n // The ._invoke method unifies the implementations of the .next,\n // .throw, and .return methods.\n generator._invoke = makeInvokeMethod(innerFn, self, context);\n\n return generator;\n }\n exports.wrap = wrap;\n\n // Try/catch helper to minimize deoptimizations. Returns a completion\n // record like context.tryEntries[i].completion. This interface could\n // have been (and was previously) designed to take a closure to be\n // invoked without arguments, but in all the cases we care about we\n // already have an existing method we want to call, so there's no need\n // to create a new function object. We can even get away with assuming\n // the method takes exactly one argument, since that happens to be true\n // in every case, so we don't have to touch the arguments object. The\n // only additional allocation required is the completion record, which\n // has a stable shape and so hopefully should be cheap to allocate.\n function tryCatch(fn, obj, arg) {\n try {\n return { type: \"normal\", arg: fn.call(obj, arg) };\n } catch (err) {\n return { type: \"throw\", arg: err };\n }\n }\n\n var GenStateSuspendedStart = \"suspendedStart\";\n var GenStateSuspendedYield = \"suspendedYield\";\n var GenStateExecuting = \"executing\";\n var GenStateCompleted = \"completed\";\n\n // Returning this object from the innerFn has the same effect as\n // breaking out of the dispatch switch statement.\n var ContinueSentinel = {};\n\n // Dummy constructor functions that we use as the .constructor and\n // .constructor.prototype properties for functions that return Generator\n // objects. For full spec compliance, you may wish to configure your\n // minifier not to mangle the names of these two functions.\n function Generator() {}\n function GeneratorFunction() {}\n function GeneratorFunctionPrototype() {}\n\n // This is a polyfill for %IteratorPrototype% for environments that\n // don't natively support it.\n var IteratorPrototype = {};\n IteratorPrototype[iteratorSymbol] = function () {\n return this;\n };\n\n var getProto = Object.getPrototypeOf;\n var NativeIteratorPrototype = getProto && getProto(getProto(values([])));\n if (NativeIteratorPrototype &&\n NativeIteratorPrototype !== Op &&\n hasOwn.call(NativeIteratorPrototype, iteratorSymbol)) {\n // This environment has a native %IteratorPrototype%; use it instead\n // of the polyfill.\n IteratorPrototype = NativeIteratorPrototype;\n }\n\n var Gp = GeneratorFunctionPrototype.prototype =\n Generator.prototype = Object.create(IteratorPrototype);\n GeneratorFunction.prototype = Gp.constructor = GeneratorFunctionPrototype;\n GeneratorFunctionPrototype.constructor = GeneratorFunction;\n GeneratorFunction.displayName = define(\n GeneratorFunctionPrototype,\n toStringTagSymbol,\n \"GeneratorFunction\"\n );\n\n // Helper for defining the .next, .throw, and .return methods of the\n // Iterator interface in terms of a single ._invoke method.\n function defineIteratorMethods(prototype) {\n [\"next\", \"throw\", \"return\"].forEach(function(method) {\n define(prototype, method, function(arg) {\n return this._invoke(method, arg);\n });\n });\n }\n\n exports.isGeneratorFunction = function(genFun) {\n var ctor = typeof genFun === \"function\" && genFun.constructor;\n return ctor\n ? ctor === GeneratorFunction ||\n // For the native GeneratorFunction constructor, the best we can\n // do is to check its .name property.\n (ctor.displayName || ctor.name) === \"GeneratorFunction\"\n : false;\n };\n\n exports.mark = function(genFun) {\n if (Object.setPrototypeOf) {\n Object.setPrototypeOf(genFun, GeneratorFunctionPrototype);\n } else {\n genFun.__proto__ = GeneratorFunctionPrototype;\n define(genFun, toStringTagSymbol, \"GeneratorFunction\");\n }\n genFun.prototype = Object.create(Gp);\n return genFun;\n };\n\n // Within the body of any async function, `await x` is transformed to\n // `yield regeneratorRuntime.awrap(x)`, so that the runtime can test\n // `hasOwn.call(value, \"__await\")` to determine if the yielded value is\n // meant to be awaited.\n exports.awrap = function(arg) {\n return { __await: arg };\n };\n\n function AsyncIterator(generator, PromiseImpl) {\n function invoke(method, arg, resolve, reject) {\n var record = tryCatch(generator[method], generator, arg);\n if (record.type === \"throw\") {\n reject(record.arg);\n } else {\n var result = record.arg;\n var value = result.value;\n if (value &&\n typeof value === \"object\" &&\n hasOwn.call(value, \"__await\")) {\n return PromiseImpl.resolve(value.__await).then(function(value) {\n invoke(\"next\", value, resolve, reject);\n }, function(err) {\n invoke(\"throw\", err, resolve, reject);\n });\n }\n\n return PromiseImpl.resolve(value).then(function(unwrapped) {\n // When a yielded Promise is resolved, its final value becomes\n // the .value of the Promise<{value,done}> result for the\n // current iteration.\n result.value = unwrapped;\n resolve(result);\n }, function(error) {\n // If a rejected Promise was yielded, throw the rejection back\n // into the async generator function so it can be handled there.\n return invoke(\"throw\", error, resolve, reject);\n });\n }\n }\n\n var previousPromise;\n\n function enqueue(method, arg) {\n function callInvokeWithMethodAndArg() {\n return new PromiseImpl(function(resolve, reject) {\n invoke(method, arg, resolve, reject);\n });\n }\n\n return previousPromise =\n // If enqueue has been called before, then we want to wait until\n // all previous Promises have been resolved before calling invoke,\n // so that results are always delivered in the correct order. If\n // enqueue has not been called before, then it is important to\n // call invoke immediately, without waiting on a callback to fire,\n // so that the async generator function has the opportunity to do\n // any necessary setup in a predictable way. This predictability\n // is why the Promise constructor synchronously invokes its\n // executor callback, and why async functions synchronously\n // execute code before the first await. Since we implement simple\n // async functions in terms of async generators, it is especially\n // important to get this right, even though it requires care.\n previousPromise ? previousPromise.then(\n callInvokeWithMethodAndArg,\n // Avoid propagating failures to Promises returned by later\n // invocations of the iterator.\n callInvokeWithMethodAndArg\n ) : callInvokeWithMethodAndArg();\n }\n\n // Define the unified helper method that is used to implement .next,\n // .throw, and .return (see defineIteratorMethods).\n this._invoke = enqueue;\n }\n\n defineIteratorMethods(AsyncIterator.prototype);\n AsyncIterator.prototype[asyncIteratorSymbol] = function () {\n return this;\n };\n exports.AsyncIterator = AsyncIterator;\n\n // Note that simple async functions are implemented on top of\n // AsyncIterator objects; they just return a Promise for the value of\n // the final result produced by the iterator.\n exports.async = function(innerFn, outerFn, self, tryLocsList, PromiseImpl) {\n if (PromiseImpl === void 0) PromiseImpl = Promise;\n\n var iter = new AsyncIterator(\n wrap(innerFn, outerFn, self, tryLocsList),\n PromiseImpl\n );\n\n return exports.isGeneratorFunction(outerFn)\n ? iter // If outerFn is a generator, return the full iterator.\n : iter.next().then(function(result) {\n return result.done ? result.value : iter.next();\n });\n };\n\n function makeInvokeMethod(innerFn, self, context) {\n var state = GenStateSuspendedStart;\n\n return function invoke(method, arg) {\n if (state === GenStateExecuting) {\n throw new Error(\"Generator is already running\");\n }\n\n if (state === GenStateCompleted) {\n if (method === \"throw\") {\n throw arg;\n }\n\n // Be forgiving, per 25.3.3.3.3 of the spec:\n // https://people.mozilla.org/~jorendorff/es6-draft.html#sec-generatorresume\n return doneResult();\n }\n\n context.method = method;\n context.arg = arg;\n\n while (true) {\n var delegate = context.delegate;\n if (delegate) {\n var delegateResult = maybeInvokeDelegate(delegate, context);\n if (delegateResult) {\n if (delegateResult === ContinueSentinel) continue;\n return delegateResult;\n }\n }\n\n if (context.method === \"next\") {\n // Setting context._sent for legacy support of Babel's\n // function.sent implementation.\n context.sent = context._sent = context.arg;\n\n } else if (context.method === \"throw\") {\n if (state === GenStateSuspendedStart) {\n state = GenStateCompleted;\n throw context.arg;\n }\n\n context.dispatchException(context.arg);\n\n } else if (context.method === \"return\") {\n context.abrupt(\"return\", context.arg);\n }\n\n state = GenStateExecuting;\n\n var record = tryCatch(innerFn, self, context);\n if (record.type === \"normal\") {\n // If an exception is thrown from innerFn, we leave state ===\n // GenStateExecuting and loop back for another invocation.\n state = context.done\n ? GenStateCompleted\n : GenStateSuspendedYield;\n\n if (record.arg === ContinueSentinel) {\n continue;\n }\n\n return {\n value: record.arg,\n done: context.done\n };\n\n } else if (record.type === \"throw\") {\n state = GenStateCompleted;\n // Dispatch the exception by looping back around to the\n // context.dispatchException(context.arg) call above.\n context.method = \"throw\";\n context.arg = record.arg;\n }\n }\n };\n }\n\n // Call delegate.iterator[context.method](context.arg) and handle the\n // result, either by returning a { value, done } result from the\n // delegate iterator, or by modifying context.method and context.arg,\n // setting context.delegate to null, and returning the ContinueSentinel.\n function maybeInvokeDelegate(delegate, context) {\n var method = delegate.iterator[context.method];\n if (method === undefined) {\n // A .throw or .return when the delegate iterator has no .throw\n // method always terminates the yield* loop.\n context.delegate = null;\n\n if (context.method === \"throw\") {\n // Note: [\"return\"] must be used for ES3 parsing compatibility.\n if (delegate.iterator[\"return\"]) {\n // If the delegate iterator has a return method, give it a\n // chance to clean up.\n context.method = \"return\";\n context.arg = undefined;\n maybeInvokeDelegate(delegate, context);\n\n if (context.method === \"throw\") {\n // If maybeInvokeDelegate(context) changed context.method from\n // \"return\" to \"throw\", let that override the TypeError below.\n return ContinueSentinel;\n }\n }\n\n context.method = \"throw\";\n context.arg = new TypeError(\n \"The iterator does not provide a 'throw' method\");\n }\n\n return ContinueSentinel;\n }\n\n var record = tryCatch(method, delegate.iterator, context.arg);\n\n if (record.type === \"throw\") {\n context.method = \"throw\";\n context.arg = record.arg;\n context.delegate = null;\n return ContinueSentinel;\n }\n\n var info = record.arg;\n\n if (! info) {\n context.method = \"throw\";\n context.arg = new TypeError(\"iterator result is not an object\");\n context.delegate = null;\n return ContinueSentinel;\n }\n\n if (info.done) {\n // Assign the result of the finished delegate to the temporary\n // variable specified by delegate.resultName (see delegateYield).\n context[delegate.resultName] = info.value;\n\n // Resume execution at the desired location (see delegateYield).\n context.next = delegate.nextLoc;\n\n // If context.method was \"throw\" but the delegate handled the\n // exception, let the outer generator proceed normally. If\n // context.method was \"next\", forget context.arg since it has been\n // \"consumed\" by the delegate iterator. If context.method was\n // \"return\", allow the original .return call to continue in the\n // outer generator.\n if (context.method !== \"return\") {\n context.method = \"next\";\n context.arg = undefined;\n }\n\n } else {\n // Re-yield the result returned by the delegate method.\n return info;\n }\n\n // The delegate iterator is finished, so forget it and continue with\n // the outer generator.\n context.delegate = null;\n return ContinueSentinel;\n }\n\n // Define Generator.prototype.{next,throw,return} in terms of the\n // unified ._invoke helper method.\n defineIteratorMethods(Gp);\n\n define(Gp, toStringTagSymbol, \"Generator\");\n\n // A Generator should always return itself as the iterator object when the\n // @@iterator function is called on it. Some browsers' implementations of the\n // iterator prototype chain incorrectly implement this, causing the Generator\n // object to not be returned from this call. This ensures that doesn't happen.\n // See https://github.com/facebook/regenerator/issues/274 for more details.\n Gp[iteratorSymbol] = function() {\n return this;\n };\n\n Gp.toString = function() {\n return \"[object Generator]\";\n };\n\n function pushTryEntry(locs) {\n var entry = { tryLoc: locs[0] };\n\n if (1 in locs) {\n entry.catchLoc = locs[1];\n }\n\n if (2 in locs) {\n entry.finallyLoc = locs[2];\n entry.afterLoc = locs[3];\n }\n\n this.tryEntries.push(entry);\n }\n\n function resetTryEntry(entry) {\n var record = entry.completion || {};\n record.type = \"normal\";\n delete record.arg;\n entry.completion = record;\n }\n\n function Context(tryLocsList) {\n // The root entry object (effectively a try statement without a catch\n // or a finally block) gives us a place to store values thrown from\n // locations where there is no enclosing try statement.\n this.tryEntries = [{ tryLoc: \"root\" }];\n tryLocsList.forEach(pushTryEntry, this);\n this.reset(true);\n }\n\n exports.keys = function(object) {\n var keys = [];\n for (var key in object) {\n keys.push(key);\n }\n keys.reverse();\n\n // Rather than returning an object with a next method, we keep\n // things simple and return the next function itself.\n return function next() {\n while (keys.length) {\n var key = keys.pop();\n if (key in object) {\n next.value = key;\n next.done = false;\n return next;\n }\n }\n\n // To avoid creating an additional object, we just hang the .value\n // and .done properties off the next function object itself. This\n // also ensures that the minifier will not anonymize the function.\n next.done = true;\n return next;\n };\n };\n\n function values(iterable) {\n if (iterable) {\n var iteratorMethod = iterable[iteratorSymbol];\n if (iteratorMethod) {\n return iteratorMethod.call(iterable);\n }\n\n if (typeof iterable.next === \"function\") {\n return iterable;\n }\n\n if (!isNaN(iterable.length)) {\n var i = -1, next = function next() {\n while (++i < iterable.length) {\n if (hasOwn.call(iterable, i)) {\n next.value = iterable[i];\n next.done = false;\n return next;\n }\n }\n\n next.value = undefined;\n next.done = true;\n\n return next;\n };\n\n return next.next = next;\n }\n }\n\n // Return an iterator with no values.\n return { next: doneResult };\n }\n exports.values = values;\n\n function doneResult() {\n return { value: undefined, done: true };\n }\n\n Context.prototype = {\n constructor: Context,\n\n reset: function(skipTempReset) {\n this.prev = 0;\n this.next = 0;\n // Resetting context._sent for legacy support of Babel's\n // function.sent implementation.\n this.sent = this._sent = undefined;\n this.done = false;\n this.delegate = null;\n\n this.method = \"next\";\n this.arg = undefined;\n\n this.tryEntries.forEach(resetTryEntry);\n\n if (!skipTempReset) {\n for (var name in this) {\n // Not sure about the optimal order of these conditions:\n if (name.charAt(0) === \"t\" &&\n hasOwn.call(this, name) &&\n !isNaN(+name.slice(1))) {\n this[name] = undefined;\n }\n }\n }\n },\n\n stop: function() {\n this.done = true;\n\n var rootEntry = this.tryEntries[0];\n var rootRecord = rootEntry.completion;\n if (rootRecord.type === \"throw\") {\n throw rootRecord.arg;\n }\n\n return this.rval;\n },\n\n dispatchException: function(exception) {\n if (this.done) {\n throw exception;\n }\n\n var context = this;\n function handle(loc, caught) {\n record.type = \"throw\";\n record.arg = exception;\n context.next = loc;\n\n if (caught) {\n // If the dispatched exception was caught by a catch block,\n // then let that catch block handle the exception normally.\n context.method = \"next\";\n context.arg = undefined;\n }\n\n return !! caught;\n }\n\n for (var i = this.tryEntries.length - 1; i >= 0; --i) {\n var entry = this.tryEntries[i];\n var record = entry.completion;\n\n if (entry.tryLoc === \"root\") {\n // Exception thrown outside of any try block that could handle\n // it, so set the completion value of the entire function to\n // throw the exception.\n return handle(\"end\");\n }\n\n if (entry.tryLoc <= this.prev) {\n var hasCatch = hasOwn.call(entry, \"catchLoc\");\n var hasFinally = hasOwn.call(entry, \"finallyLoc\");\n\n if (hasCatch && hasFinally) {\n if (this.prev < entry.catchLoc) {\n return handle(entry.catchLoc, true);\n } else if (this.prev < entry.finallyLoc) {\n return handle(entry.finallyLoc);\n }\n\n } else if (hasCatch) {\n if (this.prev < entry.catchLoc) {\n return handle(entry.catchLoc, true);\n }\n\n } else if (hasFinally) {\n if (this.prev < entry.finallyLoc) {\n return handle(entry.finallyLoc);\n }\n\n } else {\n throw new Error(\"try statement without catch or finally\");\n }\n }\n }\n },\n\n abrupt: function(type, arg) {\n for (var i = this.tryEntries.length - 1; i >= 0; --i) {\n var entry = this.tryEntries[i];\n if (entry.tryLoc <= this.prev &&\n hasOwn.call(entry, \"finallyLoc\") &&\n this.prev < entry.finallyLoc) {\n var finallyEntry = entry;\n break;\n }\n }\n\n if (finallyEntry &&\n (type === \"break\" ||\n type === \"continue\") &&\n finallyEntry.tryLoc <= arg &&\n arg <= finallyEntry.finallyLoc) {\n // Ignore the finally entry if control is not jumping to a\n // location outside the try/catch block.\n finallyEntry = null;\n }\n\n var record = finallyEntry ? finallyEntry.completion : {};\n record.type = type;\n record.arg = arg;\n\n if (finallyEntry) {\n this.method = \"next\";\n this.next = finallyEntry.finallyLoc;\n return ContinueSentinel;\n }\n\n return this.complete(record);\n },\n\n complete: function(record, afterLoc) {\n if (record.type === \"throw\") {\n throw record.arg;\n }\n\n if (record.type === \"break\" ||\n record.type === \"continue\") {\n this.next = record.arg;\n } else if (record.type === \"return\") {\n this.rval = this.arg = record.arg;\n this.method = \"return\";\n this.next = \"end\";\n } else if (record.type === \"normal\" && afterLoc) {\n this.next = afterLoc;\n }\n\n return ContinueSentinel;\n },\n\n finish: function(finallyLoc) {\n for (var i = this.tryEntries.length - 1; i >= 0; --i) {\n var entry = this.tryEntries[i];\n if (entry.finallyLoc === finallyLoc) {\n this.complete(entry.completion, entry.afterLoc);\n resetTryEntry(entry);\n return ContinueSentinel;\n }\n }\n },\n\n \"catch\": function(tryLoc) {\n for (var i = this.tryEntries.length - 1; i >= 0; --i) {\n var entry = this.tryEntries[i];\n if (entry.tryLoc === tryLoc) {\n var record = entry.completion;\n if (record.type === \"throw\") {\n var thrown = record.arg;\n resetTryEntry(entry);\n }\n return thrown;\n }\n }\n\n // The context.catch method must only be called with a location\n // argument that corresponds to a known catch block.\n throw new Error(\"illegal catch attempt\");\n },\n\n delegateYield: function(iterable, resultName, nextLoc) {\n this.delegate = {\n iterator: values(iterable),\n resultName: resultName,\n nextLoc: nextLoc\n };\n\n if (this.method === \"next\") {\n // Deliberately forget the last sent value so that we don't\n // accidentally pass it on to the delegate.\n this.arg = undefined;\n }\n\n return ContinueSentinel;\n }\n };\n\n // Regardless of whether this script is executing as a CommonJS module\n // or not, return the runtime object so that we can declare the variable\n // regeneratorRuntime in the outer scope, which allows this module to be\n // injected easily by `bin/regenerator --include-runtime script.js`.\n return exports;\n\n}(\n // If this script is executing as a CommonJS module, use module.exports\n // as the regeneratorRuntime namespace. Otherwise create a new empty\n // object. Either way, the resulting object will be used to initialize\n // the regeneratorRuntime variable at the top of this file.\n typeof module === \"object\" ? module.exports : {}\n));\n\ntry {\n regeneratorRuntime = runtime;\n} catch (accidentalStrictMode) {\n // This module should not be running in strict mode, so the above\n // assignment should always work unless something is misconfigured. Just\n // in case runtime.js accidentally runs in strict mode, we can escape\n // strict mode using a global Function call. This could conceivably fail\n // if a Content Security Policy forbids using Function, but in that case\n // the proper solution is to fix the accidental strict mode problem. If\n // you've misconfigured your bundler to force strict mode and applied a\n // CSP to forbid Function, and you're not willing to fix either of those\n // problems, please detail your unique predicament in a GitHub issue.\n Function(\"r\", \"regeneratorRuntime = r\")(runtime);\n}\n","\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.bodyOpenClassName = exports.portalClassName = undefined;\n\nvar _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };\n\nvar _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();\n\nvar _react = require(\"react\");\n\nvar _react2 = _interopRequireDefault(_react);\n\nvar _reactDom = require(\"react-dom\");\n\nvar _reactDom2 = _interopRequireDefault(_reactDom);\n\nvar _propTypes = require(\"prop-types\");\n\nvar _propTypes2 = _interopRequireDefault(_propTypes);\n\nvar _ModalPortal = require(\"./ModalPortal\");\n\nvar _ModalPortal2 = _interopRequireDefault(_ModalPortal);\n\nvar _ariaAppHider = require(\"../helpers/ariaAppHider\");\n\nvar ariaAppHider = _interopRequireWildcard(_ariaAppHider);\n\nvar _safeHTMLElement = require(\"../helpers/safeHTMLElement\");\n\nvar _safeHTMLElement2 = _interopRequireDefault(_safeHTMLElement);\n\nvar _reactLifecyclesCompat = require(\"react-lifecycles-compat\");\n\nfunction _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\"); } return call && (typeof call === \"object\" || typeof call === \"function\") ? call : self; }\n\nfunction _inherits(subClass, superClass) { if (typeof superClass !== \"function\" && superClass !== null) { throw new TypeError(\"Super expression must either be null or a function, not \" + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }\n\nvar portalClassName = exports.portalClassName = \"ReactModalPortal\";\nvar bodyOpenClassName = exports.bodyOpenClassName = \"ReactModal__Body--open\";\n\nvar isReact16 = _safeHTMLElement.canUseDOM && _reactDom2.default.createPortal !== undefined;\n\nvar createHTMLElement = function createHTMLElement(name) {\n return document.createElement(name);\n};\n\nvar getCreatePortal = function getCreatePortal() {\n return isReact16 ? _reactDom2.default.createPortal : _reactDom2.default.unstable_renderSubtreeIntoContainer;\n};\n\nfunction getParentElement(parentSelector) {\n return parentSelector();\n}\n\nvar Modal = function (_Component) {\n _inherits(Modal, _Component);\n\n function Modal() {\n var _ref;\n\n var _temp, _this, _ret;\n\n _classCallCheck(this, Modal);\n\n for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {\n args[_key] = arguments[_key];\n }\n\n return _ret = (_temp = (_this = _possibleConstructorReturn(this, (_ref = Modal.__proto__ || Object.getPrototypeOf(Modal)).call.apply(_ref, [this].concat(args))), _this), _this.removePortal = function () {\n !isReact16 && _reactDom2.default.unmountComponentAtNode(_this.node);\n var parent = getParentElement(_this.props.parentSelector);\n if (parent && parent.contains(_this.node)) {\n parent.removeChild(_this.node);\n } else {\n // eslint-disable-next-line no-console\n console.warn('React-Modal: \"parentSelector\" prop did not returned any DOM ' + \"element. Make sure that the parent element is unmounted to \" + \"avoid any memory leaks.\");\n }\n }, _this.portalRef = function (ref) {\n _this.portal = ref;\n }, _this.renderPortal = function (props) {\n var createPortal = getCreatePortal();\n var portal = createPortal(_this, _react2.default.createElement(_ModalPortal2.default, _extends({ defaultStyles: Modal.defaultStyles }, props)), _this.node);\n _this.portalRef(portal);\n }, _temp), _possibleConstructorReturn(_this, _ret);\n }\n\n _createClass(Modal, [{\n key: \"componentDidMount\",\n value: function componentDidMount() {\n if (!_safeHTMLElement.canUseDOM) return;\n\n if (!isReact16) {\n this.node = createHTMLElement(\"div\");\n }\n this.node.className = this.props.portalClassName;\n\n var parent = getParentElement(this.props.parentSelector);\n parent.appendChild(this.node);\n\n !isReact16 && this.renderPortal(this.props);\n }\n }, {\n key: \"getSnapshotBeforeUpdate\",\n value: function getSnapshotBeforeUpdate(prevProps) {\n var prevParent = getParentElement(prevProps.parentSelector);\n var nextParent = getParentElement(this.props.parentSelector);\n return { prevParent: prevParent, nextParent: nextParent };\n }\n }, {\n key: \"componentDidUpdate\",\n value: function componentDidUpdate(prevProps, _, snapshot) {\n if (!_safeHTMLElement.canUseDOM) return;\n var _props = this.props,\n isOpen = _props.isOpen,\n portalClassName = _props.portalClassName;\n\n\n if (prevProps.portalClassName !== portalClassName) {\n this.node.className = portalClassName;\n }\n\n var prevParent = snapshot.prevParent,\n nextParent = snapshot.nextParent;\n\n if (nextParent !== prevParent) {\n prevParent.removeChild(this.node);\n nextParent.appendChild(this.node);\n }\n\n // Stop unnecessary renders if modal is remaining closed\n if (!prevProps.isOpen && !isOpen) return;\n\n !isReact16 && this.renderPortal(this.props);\n }\n }, {\n key: \"componentWillUnmount\",\n value: function componentWillUnmount() {\n if (!_safeHTMLElement.canUseDOM || !this.node || !this.portal) return;\n\n var state = this.portal.state;\n var now = Date.now();\n var closesAt = state.isOpen && this.props.closeTimeoutMS && (state.closesAt || now + this.props.closeTimeoutMS);\n\n if (closesAt) {\n if (!state.beforeClose) {\n this.portal.closeWithTimeout();\n }\n\n setTimeout(this.removePortal, closesAt - now);\n } else {\n this.removePortal();\n }\n }\n }, {\n key: \"render\",\n value: function render() {\n if (!_safeHTMLElement.canUseDOM || !isReact16) {\n return null;\n }\n\n if (!this.node && isReact16) {\n this.node = createHTMLElement(\"div\");\n }\n\n var createPortal = getCreatePortal();\n return createPortal(_react2.default.createElement(_ModalPortal2.default, _extends({\n ref: this.portalRef,\n defaultStyles: Modal.defaultStyles\n }, this.props)), this.node);\n }\n }], [{\n key: \"setAppElement\",\n value: function setAppElement(element) {\n ariaAppHider.setElement(element);\n }\n\n /* eslint-disable react/no-unused-prop-types */\n\n /* eslint-enable react/no-unused-prop-types */\n\n }]);\n\n return Modal;\n}(_react.Component);\n\nModal.propTypes = {\n isOpen: _propTypes2.default.bool.isRequired,\n style: _propTypes2.default.shape({\n content: _propTypes2.default.object,\n overlay: _propTypes2.default.object\n }),\n portalClassName: _propTypes2.default.string,\n bodyOpenClassName: _propTypes2.default.string,\n htmlOpenClassName: _propTypes2.default.string,\n className: _propTypes2.default.oneOfType([_propTypes2.default.string, _propTypes2.default.shape({\n base: _propTypes2.default.string.isRequired,\n afterOpen: _propTypes2.default.string.isRequired,\n beforeClose: _propTypes2.default.string.isRequired\n })]),\n overlayClassName: _propTypes2.default.oneOfType([_propTypes2.default.string, _propTypes2.default.shape({\n base: _propTypes2.default.string.isRequired,\n afterOpen: _propTypes2.default.string.isRequired,\n beforeClose: _propTypes2.default.string.isRequired\n })]),\n appElement: _propTypes2.default.oneOfType([_propTypes2.default.instanceOf(_safeHTMLElement2.default), _propTypes2.default.instanceOf(_safeHTMLElement.SafeHTMLCollection), _propTypes2.default.instanceOf(_safeHTMLElement.SafeNodeList), _propTypes2.default.arrayOf(_propTypes2.default.instanceOf(_safeHTMLElement2.default))]),\n onAfterOpen: _propTypes2.default.func,\n onRequestClose: _propTypes2.default.func,\n closeTimeoutMS: _propTypes2.default.number,\n ariaHideApp: _propTypes2.default.bool,\n shouldFocusAfterRender: _propTypes2.default.bool,\n shouldCloseOnOverlayClick: _propTypes2.default.bool,\n shouldReturnFocusAfterClose: _propTypes2.default.bool,\n preventScroll: _propTypes2.default.bool,\n parentSelector: _propTypes2.default.func,\n aria: _propTypes2.default.object,\n data: _propTypes2.default.object,\n role: _propTypes2.default.string,\n contentLabel: _propTypes2.default.string,\n shouldCloseOnEsc: _propTypes2.default.bool,\n overlayRef: _propTypes2.default.func,\n contentRef: _propTypes2.default.func,\n id: _propTypes2.default.string,\n overlayElement: _propTypes2.default.func,\n contentElement: _propTypes2.default.func\n};\nModal.defaultProps = {\n isOpen: false,\n portalClassName: portalClassName,\n bodyOpenClassName: bodyOpenClassName,\n role: \"dialog\",\n ariaHideApp: true,\n closeTimeoutMS: 0,\n shouldFocusAfterRender: true,\n shouldCloseOnEsc: true,\n shouldCloseOnOverlayClick: true,\n shouldReturnFocusAfterClose: true,\n preventScroll: false,\n parentSelector: function parentSelector() {\n return document.body;\n },\n overlayElement: function overlayElement(props, contentEl) {\n return _react2.default.createElement(\n \"div\",\n props,\n contentEl\n );\n },\n contentElement: function contentElement(props, children) {\n return _react2.default.createElement(\n \"div\",\n props,\n children\n );\n }\n};\nModal.defaultStyles = {\n overlay: {\n position: \"fixed\",\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n backgroundColor: \"rgba(255, 255, 255, 0.75)\"\n },\n content: {\n position: \"absolute\",\n top: \"40px\",\n left: \"40px\",\n right: \"40px\",\n bottom: \"40px\",\n border: \"1px solid #ccc\",\n background: \"#fff\",\n overflow: \"auto\",\n WebkitOverflowScrolling: \"touch\",\n borderRadius: \"4px\",\n outline: \"none\",\n padding: \"20px\"\n }\n};\n\n\n(0, _reactLifecyclesCompat.polyfill)(Modal);\n\nif (process.env.NODE_ENV !== \"production\") {\n Modal.setCreateHTMLElement = function (fn) {\n return createHTMLElement = fn;\n };\n}\n\nexports.default = Modal;","/**\n * Copyright (c) 2013-present, Facebook, Inc.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n'use strict';\n\nvar ReactPropTypesSecret = require('./lib/ReactPropTypesSecret');\n\nfunction emptyFunction() {}\nfunction emptyFunctionWithReset() {}\nemptyFunctionWithReset.resetWarningCache = emptyFunction;\n\nmodule.exports = function() {\n function shim(props, propName, componentName, location, propFullName, secret) {\n if (secret === ReactPropTypesSecret) {\n // It is still safe when called from React.\n return;\n }\n var err = new Error(\n 'Calling PropTypes validators directly is not supported by the `prop-types` package. ' +\n 'Use PropTypes.checkPropTypes() to call them. ' +\n 'Read more at http://fb.me/use-check-prop-types'\n );\n err.name = 'Invariant Violation';\n throw err;\n };\n shim.isRequired = shim;\n function getShim() {\n return shim;\n };\n // Important!\n // Keep this list in sync with production version in `./factoryWithTypeCheckers.js`.\n var ReactPropTypes = {\n array: shim,\n bool: shim,\n func: shim,\n number: shim,\n object: shim,\n string: shim,\n symbol: shim,\n\n any: shim,\n arrayOf: getShim,\n element: shim,\n elementType: shim,\n instanceOf: getShim,\n node: shim,\n objectOf: getShim,\n oneOf: getShim,\n oneOfType: getShim,\n shape: getShim,\n exact: getShim,\n\n checkPropTypes: emptyFunctionWithReset,\n resetWarningCache: emptyFunction\n };\n\n ReactPropTypes.PropTypes = ReactPropTypes;\n\n return ReactPropTypes;\n};\n","/**\n * Copyright (c) 2013-present, Facebook, Inc.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n'use strict';\n\nvar ReactPropTypesSecret = 'SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED';\n\nmodule.exports = ReactPropTypesSecret;\n","\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n\nvar _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };\n\nvar _typeof = typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; };\n\nvar _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();\n\nvar _react = require(\"react\");\n\nvar _propTypes = require(\"prop-types\");\n\nvar _propTypes2 = _interopRequireDefault(_propTypes);\n\nvar _focusManager = require(\"../helpers/focusManager\");\n\nvar focusManager = _interopRequireWildcard(_focusManager);\n\nvar _scopeTab = require(\"../helpers/scopeTab\");\n\nvar _scopeTab2 = _interopRequireDefault(_scopeTab);\n\nvar _ariaAppHider = require(\"../helpers/ariaAppHider\");\n\nvar ariaAppHider = _interopRequireWildcard(_ariaAppHider);\n\nvar _classList = require(\"../helpers/classList\");\n\nvar classList = _interopRequireWildcard(_classList);\n\nvar _safeHTMLElement = require(\"../helpers/safeHTMLElement\");\n\nvar _safeHTMLElement2 = _interopRequireDefault(_safeHTMLElement);\n\nvar _portalOpenInstances = require(\"../helpers/portalOpenInstances\");\n\nvar _portalOpenInstances2 = _interopRequireDefault(_portalOpenInstances);\n\nrequire(\"../helpers/bodyTrap\");\n\nfunction _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\"); } return call && (typeof call === \"object\" || typeof call === \"function\") ? call : self; }\n\nfunction _inherits(subClass, superClass) { if (typeof superClass !== \"function\" && superClass !== null) { throw new TypeError(\"Super expression must either be null or a function, not \" + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }\n\n// so that our CSS is statically analyzable\nvar CLASS_NAMES = {\n overlay: \"ReactModal__Overlay\",\n content: \"ReactModal__Content\"\n};\n\nvar TAB_KEY = 9;\nvar ESC_KEY = 27;\n\nvar ariaHiddenInstances = 0;\n\nvar ModalPortal = function (_Component) {\n _inherits(ModalPortal, _Component);\n\n function ModalPortal(props) {\n _classCallCheck(this, ModalPortal);\n\n var _this = _possibleConstructorReturn(this, (ModalPortal.__proto__ || Object.getPrototypeOf(ModalPortal)).call(this, props));\n\n _this.setOverlayRef = function (overlay) {\n _this.overlay = overlay;\n _this.props.overlayRef && _this.props.overlayRef(overlay);\n };\n\n _this.setContentRef = function (content) {\n _this.content = content;\n _this.props.contentRef && _this.props.contentRef(content);\n };\n\n _this.afterClose = function () {\n var _this$props = _this.props,\n appElement = _this$props.appElement,\n ariaHideApp = _this$props.ariaHideApp,\n htmlOpenClassName = _this$props.htmlOpenClassName,\n bodyOpenClassName = _this$props.bodyOpenClassName;\n\n // Remove classes.\n\n bodyOpenClassName && classList.remove(document.body, bodyOpenClassName);\n\n htmlOpenClassName && classList.remove(document.getElementsByTagName(\"html\")[0], htmlOpenClassName);\n\n // Reset aria-hidden attribute if all modals have been removed\n if (ariaHideApp && ariaHiddenInstances > 0) {\n ariaHiddenInstances -= 1;\n\n if (ariaHiddenInstances === 0) {\n ariaAppHider.show(appElement);\n }\n }\n\n if (_this.props.shouldFocusAfterRender) {\n if (_this.props.shouldReturnFocusAfterClose) {\n focusManager.returnFocus(_this.props.preventScroll);\n focusManager.teardownScopedFocus();\n } else {\n focusManager.popWithoutFocus();\n }\n }\n\n if (_this.props.onAfterClose) {\n _this.props.onAfterClose();\n }\n\n _portalOpenInstances2.default.deregister(_this);\n };\n\n _this.open = function () {\n _this.beforeOpen();\n if (_this.state.afterOpen && _this.state.beforeClose) {\n clearTimeout(_this.closeTimer);\n _this.setState({ beforeClose: false });\n } else {\n if (_this.props.shouldFocusAfterRender) {\n focusManager.setupScopedFocus(_this.node);\n focusManager.markForFocusLater();\n }\n\n _this.setState({ isOpen: true }, function () {\n _this.openAnimationFrame = requestAnimationFrame(function () {\n _this.setState({ afterOpen: true });\n\n if (_this.props.isOpen && _this.props.onAfterOpen) {\n _this.props.onAfterOpen({\n overlayEl: _this.overlay,\n contentEl: _this.content\n });\n }\n });\n });\n }\n };\n\n _this.close = function () {\n if (_this.props.closeTimeoutMS > 0) {\n _this.closeWithTimeout();\n } else {\n _this.closeWithoutTimeout();\n }\n };\n\n _this.focusContent = function () {\n return _this.content && !_this.contentHasFocus() && _this.content.focus({ preventScroll: true });\n };\n\n _this.closeWithTimeout = function () {\n var closesAt = Date.now() + _this.props.closeTimeoutMS;\n _this.setState({ beforeClose: true, closesAt: closesAt }, function () {\n _this.closeTimer = setTimeout(_this.closeWithoutTimeout, _this.state.closesAt - Date.now());\n });\n };\n\n _this.closeWithoutTimeout = function () {\n _this.setState({\n beforeClose: false,\n isOpen: false,\n afterOpen: false,\n closesAt: null\n }, _this.afterClose);\n };\n\n _this.handleKeyDown = function (event) {\n if (event.keyCode === TAB_KEY) {\n (0, _scopeTab2.default)(_this.content, event);\n }\n\n if (_this.props.shouldCloseOnEsc && event.keyCode === ESC_KEY) {\n event.stopPropagation();\n _this.requestClose(event);\n }\n };\n\n _this.handleOverlayOnClick = function (event) {\n if (_this.shouldClose === null) {\n _this.shouldClose = true;\n }\n\n if (_this.shouldClose && _this.props.shouldCloseOnOverlayClick) {\n if (_this.ownerHandlesClose()) {\n _this.requestClose(event);\n } else {\n _this.focusContent();\n }\n }\n _this.shouldClose = null;\n };\n\n _this.handleContentOnMouseUp = function () {\n _this.shouldClose = false;\n };\n\n _this.handleOverlayOnMouseDown = function (event) {\n if (!_this.props.shouldCloseOnOverlayClick && event.target == _this.overlay) {\n event.preventDefault();\n }\n };\n\n _this.handleContentOnClick = function () {\n _this.shouldClose = false;\n };\n\n _this.handleContentOnMouseDown = function () {\n _this.shouldClose = false;\n };\n\n _this.requestClose = function (event) {\n return _this.ownerHandlesClose() && _this.props.onRequestClose(event);\n };\n\n _this.ownerHandlesClose = function () {\n return _this.props.onRequestClose;\n };\n\n _this.shouldBeClosed = function () {\n return !_this.state.isOpen && !_this.state.beforeClose;\n };\n\n _this.contentHasFocus = function () {\n return document.activeElement === _this.content || _this.content.contains(document.activeElement);\n };\n\n _this.buildClassName = function (which, additional) {\n var classNames = (typeof additional === \"undefined\" ? \"undefined\" : _typeof(additional)) === \"object\" ? additional : {\n base: CLASS_NAMES[which],\n afterOpen: CLASS_NAMES[which] + \"--after-open\",\n beforeClose: CLASS_NAMES[which] + \"--before-close\"\n };\n var className = classNames.base;\n if (_this.state.afterOpen) {\n className = className + \" \" + classNames.afterOpen;\n }\n if (_this.state.beforeClose) {\n className = className + \" \" + classNames.beforeClose;\n }\n return typeof additional === \"string\" && additional ? className + \" \" + additional : className;\n };\n\n _this.attributesFromObject = function (prefix, items) {\n return Object.keys(items).reduce(function (acc, name) {\n acc[prefix + \"-\" + name] = items[name];\n return acc;\n }, {});\n };\n\n _this.state = {\n afterOpen: false,\n beforeClose: false\n };\n\n _this.shouldClose = null;\n _this.moveFromContentToOverlay = null;\n return _this;\n }\n\n _createClass(ModalPortal, [{\n key: \"componentDidMount\",\n value: function componentDidMount() {\n if (this.props.isOpen) {\n this.open();\n }\n }\n }, {\n key: \"componentDidUpdate\",\n value: function componentDidUpdate(prevProps, prevState) {\n if (process.env.NODE_ENV !== \"production\") {\n if (prevProps.bodyOpenClassName !== this.props.bodyOpenClassName) {\n // eslint-disable-next-line no-console\n console.warn('React-Modal: \"bodyOpenClassName\" prop has been modified. ' + \"This may cause unexpected behavior when multiple modals are open.\");\n }\n if (prevProps.htmlOpenClassName !== this.props.htmlOpenClassName) {\n // eslint-disable-next-line no-console\n console.warn('React-Modal: \"htmlOpenClassName\" prop has been modified. ' + \"This may cause unexpected behavior when multiple modals are open.\");\n }\n }\n\n if (this.props.isOpen && !prevProps.isOpen) {\n this.open();\n } else if (!this.props.isOpen && prevProps.isOpen) {\n this.close();\n }\n\n // Focus only needs to be set once when the modal is being opened\n if (this.props.shouldFocusAfterRender && this.state.isOpen && !prevState.isOpen) {\n this.focusContent();\n }\n }\n }, {\n key: \"componentWillUnmount\",\n value: function componentWillUnmount() {\n if (this.state.isOpen) {\n this.afterClose();\n }\n clearTimeout(this.closeTimer);\n cancelAnimationFrame(this.openAnimationFrame);\n }\n }, {\n key: \"beforeOpen\",\n value: function beforeOpen() {\n var _props = this.props,\n appElement = _props.appElement,\n ariaHideApp = _props.ariaHideApp,\n htmlOpenClassName = _props.htmlOpenClassName,\n bodyOpenClassName = _props.bodyOpenClassName;\n\n // Add classes.\n\n bodyOpenClassName && classList.add(document.body, bodyOpenClassName);\n\n htmlOpenClassName && classList.add(document.getElementsByTagName(\"html\")[0], htmlOpenClassName);\n\n if (ariaHideApp) {\n ariaHiddenInstances += 1;\n ariaAppHider.hide(appElement);\n }\n\n _portalOpenInstances2.default.register(this);\n }\n\n // Don't steal focus from inner elements\n\n }, {\n key: \"render\",\n value: function render() {\n var _props2 = this.props,\n id = _props2.id,\n className = _props2.className,\n overlayClassName = _props2.overlayClassName,\n defaultStyles = _props2.defaultStyles,\n children = _props2.children;\n\n var contentStyles = className ? {} : defaultStyles.content;\n var overlayStyles = overlayClassName ? {} : defaultStyles.overlay;\n\n if (this.shouldBeClosed()) {\n return null;\n }\n\n var overlayProps = {\n ref: this.setOverlayRef,\n className: this.buildClassName(\"overlay\", overlayClassName),\n style: _extends({}, overlayStyles, this.props.style.overlay),\n onClick: this.handleOverlayOnClick,\n onMouseDown: this.handleOverlayOnMouseDown\n };\n\n var contentProps = _extends({\n id: id,\n ref: this.setContentRef,\n style: _extends({}, contentStyles, this.props.style.content),\n className: this.buildClassName(\"content\", className),\n tabIndex: \"-1\",\n onKeyDown: this.handleKeyDown,\n onMouseDown: this.handleContentOnMouseDown,\n onMouseUp: this.handleContentOnMouseUp,\n onClick: this.handleContentOnClick,\n role: this.props.role,\n \"aria-label\": this.props.contentLabel\n }, this.attributesFromObject(\"aria\", _extends({ modal: true }, this.props.aria)), this.attributesFromObject(\"data\", this.props.data || {}), {\n \"data-testid\": this.props.testId\n });\n\n var contentElement = this.props.contentElement(contentProps, children);\n return this.props.overlayElement(overlayProps, contentElement);\n }\n }]);\n\n return ModalPortal;\n}(_react.Component);\n\nModalPortal.defaultProps = {\n style: {\n overlay: {},\n content: {}\n },\n defaultStyles: {}\n};\nModalPortal.propTypes = {\n isOpen: _propTypes2.default.bool.isRequired,\n defaultStyles: _propTypes2.default.shape({\n content: _propTypes2.default.object,\n overlay: _propTypes2.default.object\n }),\n style: _propTypes2.default.shape({\n content: _propTypes2.default.object,\n overlay: _propTypes2.default.object\n }),\n className: _propTypes2.default.oneOfType([_propTypes2.default.string, _propTypes2.default.object]),\n overlayClassName: _propTypes2.default.oneOfType([_propTypes2.default.string, _propTypes2.default.object]),\n bodyOpenClassName: _propTypes2.default.string,\n htmlOpenClassName: _propTypes2.default.string,\n ariaHideApp: _propTypes2.default.bool,\n appElement: _propTypes2.default.oneOfType([_propTypes2.default.instanceOf(_safeHTMLElement2.default), _propTypes2.default.instanceOf(_safeHTMLElement.SafeHTMLCollection), _propTypes2.default.instanceOf(_safeHTMLElement.SafeNodeList), _propTypes2.default.arrayOf(_propTypes2.default.instanceOf(_safeHTMLElement2.default))]),\n onAfterOpen: _propTypes2.default.func,\n onAfterClose: _propTypes2.default.func,\n onRequestClose: _propTypes2.default.func,\n closeTimeoutMS: _propTypes2.default.number,\n shouldFocusAfterRender: _propTypes2.default.bool,\n shouldCloseOnOverlayClick: _propTypes2.default.bool,\n shouldReturnFocusAfterClose: _propTypes2.default.bool,\n preventScroll: _propTypes2.default.bool,\n role: _propTypes2.default.string,\n contentLabel: _propTypes2.default.string,\n aria: _propTypes2.default.object,\n data: _propTypes2.default.object,\n children: _propTypes2.default.node,\n shouldCloseOnEsc: _propTypes2.default.bool,\n overlayRef: _propTypes2.default.func,\n contentRef: _propTypes2.default.func,\n id: _propTypes2.default.string,\n overlayElement: _propTypes2.default.func,\n contentElement: _propTypes2.default.func,\n testId: _propTypes2.default.string\n};\nexports.default = ModalPortal;\nmodule.exports = exports[\"default\"];","\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.resetState = resetState;\nexports.log = log;\nexports.handleBlur = handleBlur;\nexports.handleFocus = handleFocus;\nexports.markForFocusLater = markForFocusLater;\nexports.returnFocus = returnFocus;\nexports.popWithoutFocus = popWithoutFocus;\nexports.setupScopedFocus = setupScopedFocus;\nexports.teardownScopedFocus = teardownScopedFocus;\n\nvar _tabbable = require(\"../helpers/tabbable\");\n\nvar _tabbable2 = _interopRequireDefault(_tabbable);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nvar focusLaterElements = [];\nvar modalElement = null;\nvar needToFocus = false;\n\n/* eslint-disable no-console */\n/* istanbul ignore next */\nfunction resetState() {\n focusLaterElements = [];\n}\n\n/* istanbul ignore next */\nfunction log() {\n if (process.env.NODE_ENV !== \"production\") {\n console.log(\"focusManager ----------\");\n focusLaterElements.forEach(function (f) {\n var check = f || {};\n console.log(check.nodeName, check.className, check.id);\n });\n console.log(\"end focusManager ----------\");\n }\n}\n/* eslint-enable no-console */\n\nfunction handleBlur() {\n needToFocus = true;\n}\n\nfunction handleFocus() {\n if (needToFocus) {\n needToFocus = false;\n if (!modalElement) {\n return;\n }\n // need to see how jQuery shims document.on('focusin') so we don't need the\n // setTimeout, firefox doesn't support focusin, if it did, we could focus\n // the element outside of a setTimeout. Side-effect of this implementation\n // is that the document.body gets focus, and then we focus our element right\n // after, seems fine.\n setTimeout(function () {\n if (modalElement.contains(document.activeElement)) {\n return;\n }\n var el = (0, _tabbable2.default)(modalElement)[0] || modalElement;\n el.focus();\n }, 0);\n }\n}\n\nfunction markForFocusLater() {\n focusLaterElements.push(document.activeElement);\n}\n\n/* eslint-disable no-console */\nfunction returnFocus() {\n var preventScroll = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;\n\n var toFocus = null;\n try {\n if (focusLaterElements.length !== 0) {\n toFocus = focusLaterElements.pop();\n toFocus.focus({ preventScroll: preventScroll });\n }\n return;\n } catch (e) {\n console.warn([\"You tried to return focus to\", toFocus, \"but it is not in the DOM anymore\"].join(\" \"));\n }\n}\n/* eslint-enable no-console */\n\nfunction popWithoutFocus() {\n focusLaterElements.length > 0 && focusLaterElements.pop();\n}\n\nfunction setupScopedFocus(element) {\n modalElement = element;\n\n if (window.addEventListener) {\n window.addEventListener(\"blur\", handleBlur, false);\n document.addEventListener(\"focus\", handleFocus, true);\n } else {\n window.attachEvent(\"onBlur\", handleBlur);\n document.attachEvent(\"onFocus\", handleFocus);\n }\n}\n\nfunction teardownScopedFocus() {\n modalElement = null;\n\n if (window.addEventListener) {\n window.removeEventListener(\"blur\", handleBlur);\n document.removeEventListener(\"focus\", handleFocus);\n } else {\n window.detachEvent(\"onBlur\", handleBlur);\n document.detachEvent(\"onFocus\", handleFocus);\n }\n}","\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = scopeTab;\n\nvar _tabbable = require(\"./tabbable\");\n\nvar _tabbable2 = _interopRequireDefault(_tabbable);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nfunction getActiveElement() {\n var el = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : document;\n\n return el.activeElement.shadowRoot ? getActiveElement(el.activeElement.shadowRoot) : el.activeElement;\n}\n\nfunction scopeTab(node, event) {\n var tabbable = (0, _tabbable2.default)(node);\n\n if (!tabbable.length) {\n // Do nothing, since there are no elements that can receive focus.\n event.preventDefault();\n return;\n }\n\n var target = void 0;\n\n var shiftKey = event.shiftKey;\n var head = tabbable[0];\n var tail = tabbable[tabbable.length - 1];\n var activeElement = getActiveElement();\n\n // proceed with default browser behavior on tab.\n // Focus on last element on shift + tab.\n if (node === activeElement) {\n if (!shiftKey) return;\n target = tail;\n }\n\n if (tail === activeElement && !shiftKey) {\n target = head;\n }\n\n if (head === activeElement && shiftKey) {\n target = tail;\n }\n\n if (target) {\n event.preventDefault();\n target.focus();\n return;\n }\n\n // Safari radio issue.\n //\n // Safari does not move the focus to the radio button,\n // so we need to force it to really walk through all elements.\n //\n // This is very error prone, since we are trying to guess\n // if it is a safari browser from the first occurence between\n // chrome or safari.\n //\n // The chrome user agent contains the first ocurrence\n // as the 'chrome/version' and later the 'safari/version'.\n var checkSafari = /(\\bChrome\\b|\\bSafari\\b)\\//.exec(navigator.userAgent);\n var isSafariDesktop = checkSafari != null && checkSafari[1] != \"Chrome\" && /\\biPod\\b|\\biPad\\b/g.exec(navigator.userAgent) == null;\n\n // If we are not in safari desktop, let the browser control\n // the focus\n if (!isSafariDesktop) return;\n\n var x = tabbable.indexOf(activeElement);\n\n if (x > -1) {\n x += shiftKey ? -1 : 1;\n }\n\n target = tabbable[x];\n\n // If the tabbable element does not exist,\n // focus head/tail based on shiftKey\n if (typeof target === \"undefined\") {\n event.preventDefault();\n target = shiftKey ? tail : head;\n target.focus();\n return;\n }\n\n event.preventDefault();\n\n target.focus();\n}\nmodule.exports = exports[\"default\"];","/**\n * Copyright (c) 2014-present, Facebook, Inc.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n'use strict';\n\n/**\n * Similar to invariant but only logs a warning if the condition is not met.\n * This can be used to log issues in development environments in critical\n * paths. Removing the logging code for production environments will keep the\n * same logic and follow the same code paths.\n */\n\nvar __DEV__ = process.env.NODE_ENV !== 'production';\n\nvar warning = function() {};\n\nif (__DEV__) {\n var printWarning = function printWarning(format, args) {\n var len = arguments.length;\n args = new Array(len > 1 ? len - 1 : 0);\n for (var key = 1; key < len; key++) {\n args[key - 1] = arguments[key];\n }\n var argIndex = 0;\n var message = 'Warning: ' +\n format.replace(/%s/g, function() {\n return args[argIndex++];\n });\n if (typeof console !== 'undefined') {\n console.error(message);\n }\n try {\n // --- Welcome to debugging React ---\n // This error was thrown as a convenience so that you can use this stack\n // to find the callsite that caused this warning to fire.\n throw new Error(message);\n } catch (x) {}\n }\n\n warning = function(condition, format, args) {\n var len = arguments.length;\n args = new Array(len > 2 ? len - 2 : 0);\n for (var key = 2; key < len; key++) {\n args[key - 2] = arguments[key];\n }\n if (format === undefined) {\n throw new Error(\n '`warning(condition, format, ...args)` requires a warning ' +\n 'message argument'\n );\n }\n if (!condition) {\n printWarning.apply(null, [format].concat(args));\n }\n };\n}\n\nmodule.exports = warning;\n","/*!\n Copyright (c) 2015 Jed Watson.\n Based on code that is Copyright 2013-2015, Facebook, Inc.\n All rights reserved.\n*/\n/* global define */\n\n(function () {\n\t'use strict';\n\n\tvar canUseDOM = !!(\n\t\ttypeof window !== 'undefined' &&\n\t\twindow.document &&\n\t\twindow.document.createElement\n\t);\n\n\tvar ExecutionEnvironment = {\n\n\t\tcanUseDOM: canUseDOM,\n\n\t\tcanUseWorkers: typeof Worker !== 'undefined',\n\n\t\tcanUseEventListeners:\n\t\t\tcanUseDOM && !!(window.addEventListener || window.attachEvent),\n\n\t\tcanUseViewport: canUseDOM && !!window.screen\n\n\t};\n\n\tif (typeof define === 'function' && typeof define.amd === 'object' && define.amd) {\n\t\tdefine(function () {\n\t\t\treturn ExecutionEnvironment;\n\t\t});\n\t} else if (typeof module !== 'undefined' && module.exports) {\n\t\tmodule.exports = ExecutionEnvironment;\n\t} else {\n\t\twindow.ExecutionEnvironment = ExecutionEnvironment;\n\t}\n\n}());\n","\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.resetState = resetState;\nexports.log = log;\nvar htmlClassList = {};\nvar docBodyClassList = {};\n\n/* eslint-disable no-console */\n/* istanbul ignore next */\nfunction removeClass(at, cls) {\n at.classList.remove(cls);\n}\n\n/* istanbul ignore next */\nfunction resetState() {\n var htmlElement = document.getElementsByTagName(\"html\")[0];\n for (var cls in htmlClassList) {\n removeClass(htmlElement, htmlClassList[cls]);\n }\n\n var body = document.body;\n for (var _cls in docBodyClassList) {\n removeClass(body, docBodyClassList[_cls]);\n }\n\n htmlClassList = {};\n docBodyClassList = {};\n}\n\n/* istanbul ignore next */\nfunction log() {\n if (process.env.NODE_ENV !== \"production\") {\n var classes = document.getElementsByTagName(\"html\")[0].className;\n var buffer = \"Show tracked classes:\\n\\n\";\n\n buffer += \" (\" + classes + \"):\\n \";\n for (var x in htmlClassList) {\n buffer += \" \" + x + \" \" + htmlClassList[x] + \"\\n \";\n }\n\n classes = document.body.className;\n\n buffer += \"\\n\\ndoc.body (\" + classes + \"):\\n \";\n for (var _x in docBodyClassList) {\n buffer += \" \" + _x + \" \" + docBodyClassList[_x] + \"\\n \";\n }\n\n buffer += \"\\n\";\n\n console.log(buffer);\n }\n}\n/* eslint-enable no-console */\n\n/**\n * Track the number of reference of a class.\n * @param {object} poll The poll to receive the reference.\n * @param {string} className The class name.\n * @return {string}\n */\nvar incrementReference = function incrementReference(poll, className) {\n if (!poll[className]) {\n poll[className] = 0;\n }\n poll[className] += 1;\n return className;\n};\n\n/**\n * Drop the reference of a class.\n * @param {object} poll The poll to receive the reference.\n * @param {string} className The class name.\n * @return {string}\n */\nvar decrementReference = function decrementReference(poll, className) {\n if (poll[className]) {\n poll[className] -= 1;\n }\n return className;\n};\n\n/**\n * Track a class and add to the given class list.\n * @param {Object} classListRef A class list of an element.\n * @param {Object} poll The poll to be used.\n * @param {Array} classes The list of classes to be tracked.\n */\nvar trackClass = function trackClass(classListRef, poll, classes) {\n classes.forEach(function (className) {\n incrementReference(poll, className);\n classListRef.add(className);\n });\n};\n\n/**\n * Untrack a class and remove from the given class list if the reference\n * reaches 0.\n * @param {Object} classListRef A class list of an element.\n * @param {Object} poll The poll to be used.\n * @param {Array} classes The list of classes to be untracked.\n */\nvar untrackClass = function untrackClass(classListRef, poll, classes) {\n classes.forEach(function (className) {\n decrementReference(poll, className);\n poll[className] === 0 && classListRef.remove(className);\n });\n};\n\n/**\n * Public inferface to add classes to the document.body.\n * @param {string} bodyClass The class string to be added.\n * It may contain more then one class\n * with ' ' as separator.\n */\nvar add = exports.add = function add(element, classString) {\n return trackClass(element.classList, element.nodeName.toLowerCase() == \"html\" ? htmlClassList : docBodyClassList, classString.split(\" \"));\n};\n\n/**\n * Public inferface to remove classes from the document.body.\n * @param {string} bodyClass The class string to be added.\n * It may contain more then one class\n * with ' ' as separator.\n */\nvar remove = exports.remove = function remove(element, classString) {\n return untrackClass(element.classList, element.nodeName.toLowerCase() == \"html\" ? htmlClassList : docBodyClassList, classString.split(\" \"));\n};","\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.resetState = resetState;\nexports.log = log;\n\nvar _portalOpenInstances = require(\"./portalOpenInstances\");\n\nvar _portalOpenInstances2 = _interopRequireDefault(_portalOpenInstances);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\n// Body focus trap see Issue #742\n\nvar before = void 0,\n after = void 0,\n instances = [];\n\n/* eslint-disable no-console */\n/* istanbul ignore next */\nfunction resetState() {\n var _arr = [before, after];\n\n for (var _i = 0; _i < _arr.length; _i++) {\n var item = _arr[_i];\n if (!item) continue;\n item.parentNode && item.parentNode.removeChild(item);\n }\n before = after = null;\n instances = [];\n}\n\n/* istanbul ignore next */\nfunction log() {\n console.log(\"bodyTrap ----------\");\n console.log(instances.length);\n var _arr2 = [before, after];\n for (var _i2 = 0; _i2 < _arr2.length; _i2++) {\n var item = _arr2[_i2];\n var check = item || {};\n console.log(check.nodeName, check.className, check.id);\n }\n console.log(\"edn bodyTrap ----------\");\n}\n/* eslint-enable no-console */\n\nfunction focusContent() {\n if (instances.length === 0) {\n if (process.env.NODE_ENV !== \"production\") {\n // eslint-disable-next-line no-console\n console.warn(\"React-Modal: Open instances > 0 expected\");\n }\n return;\n }\n instances[instances.length - 1].focusContent();\n}\n\nfunction bodyTrap(eventType, openInstances) {\n if (!before && !after) {\n before = document.createElement(\"div\");\n before.setAttribute(\"data-react-modal-body-trap\", \"\");\n before.style.position = \"absolute\";\n before.style.opacity = \"0\";\n before.setAttribute(\"tabindex\", \"0\");\n before.addEventListener(\"focus\", focusContent);\n after = before.cloneNode();\n after.addEventListener(\"focus\", focusContent);\n }\n\n instances = openInstances;\n\n if (instances.length > 0) {\n // Add focus trap\n if (document.body.firstChild !== before) {\n document.body.insertBefore(before, document.body.firstChild);\n }\n if (document.body.lastChild !== after) {\n document.body.appendChild(after);\n }\n } else {\n // Remove focus trap\n if (before.parentElement) {\n before.parentElement.removeChild(before);\n }\n if (after.parentElement) {\n after.parentElement.removeChild(after);\n }\n }\n}\n\n_portalOpenInstances2.default.subscribe(bodyTrap);","/**\n * Copyright (c) 2013-present, Facebook, Inc.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nfunction componentWillMount() {\n // Call this.constructor.gDSFP to support sub-classes.\n var state = this.constructor.getDerivedStateFromProps(this.props, this.state);\n if (state !== null && state !== undefined) {\n this.setState(state);\n }\n}\n\nfunction componentWillReceiveProps(nextProps) {\n // Call this.constructor.gDSFP to support sub-classes.\n // Use the setState() updater to ensure state isn't stale in certain edge cases.\n function updater(prevState) {\n var state = this.constructor.getDerivedStateFromProps(nextProps, prevState);\n return state !== null && state !== undefined ? state : null;\n }\n // Binding \"this\" is important for shallow renderer support.\n this.setState(updater.bind(this));\n}\n\nfunction componentWillUpdate(nextProps, nextState) {\n try {\n var prevProps = this.props;\n var prevState = this.state;\n this.props = nextProps;\n this.state = nextState;\n this.__reactInternalSnapshotFlag = true;\n this.__reactInternalSnapshot = this.getSnapshotBeforeUpdate(\n prevProps,\n prevState\n );\n } finally {\n this.props = prevProps;\n this.state = prevState;\n }\n}\n\n// React may warn about cWM/cWRP/cWU methods being deprecated.\n// Add a flag to suppress these warnings for this special case.\ncomponentWillMount.__suppressDeprecationWarning = true;\ncomponentWillReceiveProps.__suppressDeprecationWarning = true;\ncomponentWillUpdate.__suppressDeprecationWarning = true;\n\nfunction polyfill(Component) {\n var prototype = Component.prototype;\n\n if (!prototype || !prototype.isReactComponent) {\n throw new Error('Can only polyfill class components');\n }\n\n if (\n typeof Component.getDerivedStateFromProps !== 'function' &&\n typeof prototype.getSnapshotBeforeUpdate !== 'function'\n ) {\n return Component;\n }\n\n // If new component APIs are defined, \"unsafe\" lifecycles won't be called.\n // Error if any of these lifecycles are present,\n // Because they would work differently between older and newer (16.3+) versions of React.\n var foundWillMountName = null;\n var foundWillReceivePropsName = null;\n var foundWillUpdateName = null;\n if (typeof prototype.componentWillMount === 'function') {\n foundWillMountName = 'componentWillMount';\n } else if (typeof prototype.UNSAFE_componentWillMount === 'function') {\n foundWillMountName = 'UNSAFE_componentWillMount';\n }\n if (typeof prototype.componentWillReceiveProps === 'function') {\n foundWillReceivePropsName = 'componentWillReceiveProps';\n } else if (typeof prototype.UNSAFE_componentWillReceiveProps === 'function') {\n foundWillReceivePropsName = 'UNSAFE_componentWillReceiveProps';\n }\n if (typeof prototype.componentWillUpdate === 'function') {\n foundWillUpdateName = 'componentWillUpdate';\n } else if (typeof prototype.UNSAFE_componentWillUpdate === 'function') {\n foundWillUpdateName = 'UNSAFE_componentWillUpdate';\n }\n if (\n foundWillMountName !== null ||\n foundWillReceivePropsName !== null ||\n foundWillUpdateName !== null\n ) {\n var componentName = Component.displayName || Component.name;\n var newApiName =\n typeof Component.getDerivedStateFromProps === 'function'\n ? 'getDerivedStateFromProps()'\n : 'getSnapshotBeforeUpdate()';\n\n throw Error(\n 'Unsafe legacy lifecycles will not be called for components using new component APIs.\\n\\n' +\n componentName +\n ' uses ' +\n newApiName +\n ' but also contains the following legacy lifecycles:' +\n (foundWillMountName !== null ? '\\n ' + foundWillMountName : '') +\n (foundWillReceivePropsName !== null\n ? '\\n ' + foundWillReceivePropsName\n : '') +\n (foundWillUpdateName !== null ? '\\n ' + foundWillUpdateName : '') +\n '\\n\\nThe above lifecycles should be removed. Learn more about this warning here:\\n' +\n 'https://fb.me/react-async-component-lifecycle-hooks'\n );\n }\n\n // React <= 16.2 does not support static getDerivedStateFromProps.\n // As a workaround, use cWM and cWRP to invoke the new static lifecycle.\n // Newer versions of React will ignore these lifecycles if gDSFP exists.\n if (typeof Component.getDerivedStateFromProps === 'function') {\n prototype.componentWillMount = componentWillMount;\n prototype.componentWillReceiveProps = componentWillReceiveProps;\n }\n\n // React <= 16.2 does not support getSnapshotBeforeUpdate.\n // As a workaround, use cWU to invoke the new lifecycle.\n // Newer versions of React will ignore that lifecycle if gSBU exists.\n if (typeof prototype.getSnapshotBeforeUpdate === 'function') {\n if (typeof prototype.componentDidUpdate !== 'function') {\n throw new Error(\n 'Cannot polyfill getSnapshotBeforeUpdate() for components that do not define componentDidUpdate() on the prototype'\n );\n }\n\n prototype.componentWillUpdate = componentWillUpdate;\n\n var componentDidUpdate = prototype.componentDidUpdate;\n\n prototype.componentDidUpdate = function componentDidUpdatePolyfill(\n prevProps,\n prevState,\n maybeSnapshot\n ) {\n // 16.3+ will not execute our will-update method;\n // It will pass a snapshot value to did-update though.\n // Older versions will require our polyfilled will-update value.\n // We need to handle both cases, but can't just check for the presence of \"maybeSnapshot\",\n // Because for <= 15.x versions this might be a \"prevContext\" object.\n // We also can't just check \"__reactInternalSnapshot\",\n // Because get-snapshot might return a falsy value.\n // So check for the explicit __reactInternalSnapshotFlag flag to determine behavior.\n var snapshot = this.__reactInternalSnapshotFlag\n ? this.__reactInternalSnapshot\n : maybeSnapshot;\n\n componentDidUpdate.call(this, prevProps, prevState, snapshot);\n };\n }\n\n return Component;\n}\n\nexport { polyfill };\n","'use strict';\n\nvar utils = require('./utils');\nvar bind = require('./helpers/bind');\nvar Axios = require('./core/Axios');\nvar mergeConfig = require('./core/mergeConfig');\nvar defaults = require('./defaults');\n\n/**\n * Create an instance of Axios\n *\n * @param {Object} defaultConfig The default config for the instance\n * @return {Axios} A new instance of Axios\n */\nfunction createInstance(defaultConfig) {\n var context = new Axios(defaultConfig);\n var instance = bind(Axios.prototype.request, context);\n\n // Copy axios.prototype to instance\n utils.extend(instance, Axios.prototype, context);\n\n // Copy context to instance\n utils.extend(instance, context);\n\n // Factory for creating new instances\n instance.create = function create(instanceConfig) {\n return createInstance(mergeConfig(defaultConfig, instanceConfig));\n };\n\n return instance;\n}\n\n// Create the default instance to be exported\nvar axios = createInstance(defaults);\n\n// Expose Axios class to allow class inheritance\naxios.Axios = Axios;\n\n// Expose Cancel & CancelToken\naxios.Cancel = require('./cancel/Cancel');\naxios.CancelToken = require('./cancel/CancelToken');\naxios.isCancel = require('./cancel/isCancel');\naxios.VERSION = require('./env/data').version;\n\n// Expose all/spread\naxios.all = function all(promises) {\n return Promise.all(promises);\n};\naxios.spread = require('./helpers/spread');\n\n// Expose isAxiosError\naxios.isAxiosError = require('./helpers/isAxiosError');\n\nmodule.exports = axios;\n\n// Allow use of default import syntax in TypeScript\nmodule.exports.default = axios;\n","'use strict';\n\nvar utils = require('./../utils');\nvar buildURL = require('../helpers/buildURL');\nvar InterceptorManager = require('./InterceptorManager');\nvar dispatchRequest = require('./dispatchRequest');\nvar mergeConfig = require('./mergeConfig');\nvar validator = require('../helpers/validator');\n\nvar validators = validator.validators;\n/**\n * Create a new instance of Axios\n *\n * @param {Object} instanceConfig The default config for the instance\n */\nfunction Axios(instanceConfig) {\n this.defaults = instanceConfig;\n this.interceptors = {\n request: new InterceptorManager(),\n response: new InterceptorManager()\n };\n}\n\n/**\n * Dispatch a request\n *\n * @param {Object} config The config specific for this request (merged with this.defaults)\n */\nAxios.prototype.request = function request(config) {\n /*eslint no-param-reassign:0*/\n // Allow for axios('example/url'[, config]) a la fetch API\n if (typeof config === 'string') {\n config = arguments[1] || {};\n config.url = arguments[0];\n } else {\n config = config || {};\n }\n\n config = mergeConfig(this.defaults, config);\n\n // Set config.method\n if (config.method) {\n config.method = config.method.toLowerCase();\n } else if (this.defaults.method) {\n config.method = this.defaults.method.toLowerCase();\n } else {\n config.method = 'get';\n }\n\n var transitional = config.transitional;\n\n if (transitional !== undefined) {\n validator.assertOptions(transitional, {\n silentJSONParsing: validators.transitional(validators.boolean),\n forcedJSONParsing: validators.transitional(validators.boolean),\n clarifyTimeoutError: validators.transitional(validators.boolean)\n }, false);\n }\n\n // filter out skipped interceptors\n var requestInterceptorChain = [];\n var synchronousRequestInterceptors = true;\n this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {\n if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {\n return;\n }\n\n synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;\n\n requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);\n });\n\n var responseInterceptorChain = [];\n this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {\n responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);\n });\n\n var promise;\n\n if (!synchronousRequestInterceptors) {\n var chain = [dispatchRequest, undefined];\n\n Array.prototype.unshift.apply(chain, requestInterceptorChain);\n chain = chain.concat(responseInterceptorChain);\n\n promise = Promise.resolve(config);\n while (chain.length) {\n promise = promise.then(chain.shift(), chain.shift());\n }\n\n return promise;\n }\n\n\n var newConfig = config;\n while (requestInterceptorChain.length) {\n var onFulfilled = requestInterceptorChain.shift();\n var onRejected = requestInterceptorChain.shift();\n try {\n newConfig = onFulfilled(newConfig);\n } catch (error) {\n onRejected(error);\n break;\n }\n }\n\n try {\n promise = dispatchRequest(newConfig);\n } catch (error) {\n return Promise.reject(error);\n }\n\n while (responseInterceptorChain.length) {\n promise = promise.then(responseInterceptorChain.shift(), responseInterceptorChain.shift());\n }\n\n return promise;\n};\n\nAxios.prototype.getUri = function getUri(config) {\n config = mergeConfig(this.defaults, config);\n return buildURL(config.url, config.params, config.paramsSerializer).replace(/^\\?/, '');\n};\n\n// Provide aliases for supported request methods\nutils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {\n /*eslint func-names:0*/\n Axios.prototype[method] = function(url, config) {\n return this.request(mergeConfig(config || {}, {\n method: method,\n url: url,\n data: (config || {}).data\n }));\n };\n});\n\nutils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {\n /*eslint func-names:0*/\n Axios.prototype[method] = function(url, data, config) {\n return this.request(mergeConfig(config || {}, {\n method: method,\n url: url,\n data: data\n }));\n };\n});\n\nmodule.exports = Axios;\n","'use strict';\n\nvar utils = require('./../utils');\n\nfunction InterceptorManager() {\n this.handlers = [];\n}\n\n/**\n * Add a new interceptor to the stack\n *\n * @param {Function} fulfilled The function to handle `then` for a `Promise`\n * @param {Function} rejected The function to handle `reject` for a `Promise`\n *\n * @return {Number} An ID used to remove interceptor later\n */\nInterceptorManager.prototype.use = function use(fulfilled, rejected, options) {\n this.handlers.push({\n fulfilled: fulfilled,\n rejected: rejected,\n synchronous: options ? options.synchronous : false,\n runWhen: options ? options.runWhen : null\n });\n return this.handlers.length - 1;\n};\n\n/**\n * Remove an interceptor from the stack\n *\n * @param {Number} id The ID that was returned by `use`\n */\nInterceptorManager.prototype.eject = function eject(id) {\n if (this.handlers[id]) {\n this.handlers[id] = null;\n }\n};\n\n/**\n * Iterate over all the registered interceptors\n *\n * This method is particularly useful for skipping over any\n * interceptors that may have become `null` calling `eject`.\n *\n * @param {Function} fn The function to call for each interceptor\n */\nInterceptorManager.prototype.forEach = function forEach(fn) {\n utils.forEach(this.handlers, function forEachHandler(h) {\n if (h !== null) {\n fn(h);\n }\n });\n};\n\nmodule.exports = InterceptorManager;\n","'use strict';\n\nvar utils = require('./../utils');\nvar transformData = require('./transformData');\nvar isCancel = require('../cancel/isCancel');\nvar defaults = require('../defaults');\nvar Cancel = require('../cancel/Cancel');\n\n/**\n * Throws a `Cancel` if cancellation has been requested.\n */\nfunction throwIfCancellationRequested(config) {\n if (config.cancelToken) {\n config.cancelToken.throwIfRequested();\n }\n\n if (config.signal && config.signal.aborted) {\n throw new Cancel('canceled');\n }\n}\n\n/**\n * Dispatch a request to the server using the configured adapter.\n *\n * @param {object} config The config that is to be used for the request\n * @returns {Promise} The Promise to be fulfilled\n */\nmodule.exports = function dispatchRequest(config) {\n throwIfCancellationRequested(config);\n\n // Ensure headers exist\n config.headers = config.headers || {};\n\n // Transform request data\n config.data = transformData.call(\n config,\n config.data,\n config.headers,\n config.transformRequest\n );\n\n // Flatten headers\n config.headers = utils.merge(\n config.headers.common || {},\n config.headers[config.method] || {},\n config.headers\n );\n\n utils.forEach(\n ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],\n function cleanHeaderConfig(method) {\n delete config.headers[method];\n }\n );\n\n var adapter = config.adapter || defaults.adapter;\n\n return adapter(config).then(function onAdapterResolution(response) {\n throwIfCancellationRequested(config);\n\n // Transform response data\n response.data = transformData.call(\n config,\n response.data,\n response.headers,\n config.transformResponse\n );\n\n return response;\n }, function onAdapterRejection(reason) {\n if (!isCancel(reason)) {\n throwIfCancellationRequested(config);\n\n // Transform response data\n if (reason && reason.response) {\n reason.response.data = transformData.call(\n config,\n reason.response.data,\n reason.response.headers,\n config.transformResponse\n );\n }\n }\n\n return Promise.reject(reason);\n });\n};\n","'use strict';\n\nvar utils = require('./../utils');\nvar defaults = require('./../defaults');\n\n/**\n * Transform the data for a request or a response\n *\n * @param {Object|String} data The data to be transformed\n * @param {Array} headers The headers for the request or response\n * @param {Array|Function} fns A single function or Array of functions\n * @returns {*} The resulting transformed data\n */\nmodule.exports = function transformData(data, headers, fns) {\n var context = this || defaults;\n /*eslint no-param-reassign:0*/\n utils.forEach(fns, function transform(fn) {\n data = fn.call(context, data, headers);\n });\n\n return data;\n};\n","'use strict';\n\nvar utils = require('../utils');\n\nmodule.exports = function normalizeHeaderName(headers, normalizedName) {\n utils.forEach(headers, function processHeader(value, name) {\n if (name !== normalizedName && name.toUpperCase() === normalizedName.toUpperCase()) {\n headers[normalizedName] = value;\n delete headers[name];\n }\n });\n};\n","'use strict';\n\nvar createError = require('./createError');\n\n/**\n * Resolve or reject a Promise based on response status.\n *\n * @param {Function} resolve A function that resolves the promise.\n * @param {Function} reject A function that rejects the promise.\n * @param {object} response The response.\n */\nmodule.exports = function settle(resolve, reject, response) {\n var validateStatus = response.config.validateStatus;\n if (!response.status || !validateStatus || validateStatus(response.status)) {\n resolve(response);\n } else {\n reject(createError(\n 'Request failed with status code ' + response.status,\n response.config,\n null,\n response.request,\n response\n ));\n }\n};\n","'use strict';\n\nvar utils = require('./../utils');\n\nmodule.exports = (\n utils.isStandardBrowserEnv() ?\n\n // Standard browser envs support document.cookie\n (function standardBrowserEnv() {\n return {\n write: function write(name, value, expires, path, domain, secure) {\n var cookie = [];\n cookie.push(name + '=' + encodeURIComponent(value));\n\n if (utils.isNumber(expires)) {\n cookie.push('expires=' + new Date(expires).toGMTString());\n }\n\n if (utils.isString(path)) {\n cookie.push('path=' + path);\n }\n\n if (utils.isString(domain)) {\n cookie.push('domain=' + domain);\n }\n\n if (secure === true) {\n cookie.push('secure');\n }\n\n document.cookie = cookie.join('; ');\n },\n\n read: function read(name) {\n var match = document.cookie.match(new RegExp('(^|;\\\\s*)(' + name + ')=([^;]*)'));\n return (match ? decodeURIComponent(match[3]) : null);\n },\n\n remove: function remove(name) {\n this.write(name, '', Date.now() - 86400000);\n }\n };\n })() :\n\n // Non standard browser env (web workers, react-native) lack needed support.\n (function nonStandardBrowserEnv() {\n return {\n write: function write() {},\n read: function read() { return null; },\n remove: function remove() {}\n };\n })()\n);\n","'use strict';\n\nvar isAbsoluteURL = require('../helpers/isAbsoluteURL');\nvar combineURLs = require('../helpers/combineURLs');\n\n/**\n * Creates a new URL by combining the baseURL with the requestedURL,\n * only when the requestedURL is not already an absolute URL.\n * If the requestURL is absolute, this function returns the requestedURL untouched.\n *\n * @param {string} baseURL The base URL\n * @param {string} requestedURL Absolute or relative URL to combine\n * @returns {string} The combined full path\n */\nmodule.exports = function buildFullPath(baseURL, requestedURL) {\n if (baseURL && !isAbsoluteURL(requestedURL)) {\n return combineURLs(baseURL, requestedURL);\n }\n return requestedURL;\n};\n","'use strict';\n\n/**\n * Determines whether the specified URL is absolute\n *\n * @param {string} url The URL to test\n * @returns {boolean} True if the specified URL is absolute, otherwise false\n */\nmodule.exports = function isAbsoluteURL(url) {\n // A URL is considered absolute if it begins with \"://\" or \"//\" (protocol-relative URL).\n // RFC 3986 defines scheme name as a sequence of characters beginning with a letter and followed\n // by any combination of letters, digits, plus, period, or hyphen.\n return /^([a-z][a-z\\d\\+\\-\\.]*:)?\\/\\//i.test(url);\n};\n","'use strict';\n\n/**\n * Creates a new URL by combining the specified URLs\n *\n * @param {string} baseURL The base URL\n * @param {string} relativeURL The relative URL\n * @returns {string} The combined URL\n */\nmodule.exports = function combineURLs(baseURL, relativeURL) {\n return relativeURL\n ? baseURL.replace(/\\/+$/, '') + '/' + relativeURL.replace(/^\\/+/, '')\n : baseURL;\n};\n","'use strict';\n\nvar utils = require('./../utils');\n\n// Headers whose duplicates are ignored by node\n// c.f. https://nodejs.org/api/http.html#http_message_headers\nvar ignoreDuplicateOf = [\n 'age', 'authorization', 'content-length', 'content-type', 'etag',\n 'expires', 'from', 'host', 'if-modified-since', 'if-unmodified-since',\n 'last-modified', 'location', 'max-forwards', 'proxy-authorization',\n 'referer', 'retry-after', 'user-agent'\n];\n\n/**\n * Parse headers into an object\n *\n * ```\n * Date: Wed, 27 Aug 2014 08:58:49 GMT\n * Content-Type: application/json\n * Connection: keep-alive\n * Transfer-Encoding: chunked\n * ```\n *\n * @param {String} headers Headers needing to be parsed\n * @returns {Object} Headers parsed into an object\n */\nmodule.exports = function parseHeaders(headers) {\n var parsed = {};\n var key;\n var val;\n var i;\n\n if (!headers) { return parsed; }\n\n utils.forEach(headers.split('\\n'), function parser(line) {\n i = line.indexOf(':');\n key = utils.trim(line.substr(0, i)).toLowerCase();\n val = utils.trim(line.substr(i + 1));\n\n if (key) {\n if (parsed[key] && ignoreDuplicateOf.indexOf(key) >= 0) {\n return;\n }\n if (key === 'set-cookie') {\n parsed[key] = (parsed[key] ? parsed[key] : []).concat([val]);\n } else {\n parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val;\n }\n }\n });\n\n return parsed;\n};\n","'use strict';\n\nvar utils = require('./../utils');\n\nmodule.exports = (\n utils.isStandardBrowserEnv() ?\n\n // Standard browser envs have full support of the APIs needed to test\n // whether the request URL is of the same origin as current location.\n (function standardBrowserEnv() {\n var msie = /(msie|trident)/i.test(navigator.userAgent);\n var urlParsingNode = document.createElement('a');\n var originURL;\n\n /**\n * Parse a URL to discover it's components\n *\n * @param {String} url The URL to be parsed\n * @returns {Object}\n */\n function resolveURL(url) {\n var href = url;\n\n if (msie) {\n // IE needs attribute set twice to normalize properties\n urlParsingNode.setAttribute('href', href);\n href = urlParsingNode.href;\n }\n\n urlParsingNode.setAttribute('href', href);\n\n // urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils\n return {\n href: urlParsingNode.href,\n protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '',\n host: urlParsingNode.host,\n search: urlParsingNode.search ? urlParsingNode.search.replace(/^\\?/, '') : '',\n hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '',\n hostname: urlParsingNode.hostname,\n port: urlParsingNode.port,\n pathname: (urlParsingNode.pathname.charAt(0) === '/') ?\n urlParsingNode.pathname :\n '/' + urlParsingNode.pathname\n };\n }\n\n originURL = resolveURL(window.location.href);\n\n /**\n * Determine if a URL shares the same origin as the current location\n *\n * @param {String} requestURL The URL to test\n * @returns {boolean} True if URL shares the same origin, otherwise false\n */\n return function isURLSameOrigin(requestURL) {\n var parsed = (utils.isString(requestURL)) ? resolveURL(requestURL) : requestURL;\n return (parsed.protocol === originURL.protocol &&\n parsed.host === originURL.host);\n };\n })() :\n\n // Non standard browser envs (web workers, react-native) lack needed support.\n (function nonStandardBrowserEnv() {\n return function isURLSameOrigin() {\n return true;\n };\n })()\n);\n","'use strict';\n\nvar VERSION = require('../env/data').version;\n\nvar validators = {};\n\n// eslint-disable-next-line func-names\n['object', 'boolean', 'number', 'function', 'string', 'symbol'].forEach(function(type, i) {\n validators[type] = function validator(thing) {\n return typeof thing === type || 'a' + (i < 1 ? 'n ' : ' ') + type;\n };\n});\n\nvar deprecatedWarnings = {};\n\n/**\n * Transitional option validator\n * @param {function|boolean?} validator - set to false if the transitional option has been removed\n * @param {string?} version - deprecated version / removed since version\n * @param {string?} message - some message with additional info\n * @returns {function}\n */\nvalidators.transitional = function transitional(validator, version, message) {\n function formatMessage(opt, desc) {\n return '[Axios v' + VERSION + '] Transitional option \\'' + opt + '\\'' + desc + (message ? '. ' + message : '');\n }\n\n // eslint-disable-next-line func-names\n return function(value, opt, opts) {\n if (validator === false) {\n throw new Error(formatMessage(opt, ' has been removed' + (version ? ' in ' + version : '')));\n }\n\n if (version && !deprecatedWarnings[opt]) {\n deprecatedWarnings[opt] = true;\n // eslint-disable-next-line no-console\n console.warn(\n formatMessage(\n opt,\n ' has been deprecated since v' + version + ' and will be removed in the near future'\n )\n );\n }\n\n return validator ? validator(value, opt, opts) : true;\n };\n};\n\n/**\n * Assert object's properties type\n * @param {object} options\n * @param {object} schema\n * @param {boolean?} allowUnknown\n */\n\nfunction assertOptions(options, schema, allowUnknown) {\n if (typeof options !== 'object') {\n throw new TypeError('options must be an object');\n }\n var keys = Object.keys(options);\n var i = keys.length;\n while (i-- > 0) {\n var opt = keys[i];\n var validator = schema[opt];\n if (validator) {\n var value = options[opt];\n var result = value === undefined || validator(value, opt, options);\n if (result !== true) {\n throw new TypeError('option ' + opt + ' must be ' + result);\n }\n continue;\n }\n if (allowUnknown !== true) {\n throw Error('Unknown option ' + opt);\n }\n }\n}\n\nmodule.exports = {\n assertOptions: assertOptions,\n validators: validators\n};\n","'use strict';\n\nvar Cancel = require('./Cancel');\n\n/**\n * A `CancelToken` is an object that can be used to request cancellation of an operation.\n *\n * @class\n * @param {Function} executor The executor function.\n */\nfunction CancelToken(executor) {\n if (typeof executor !== 'function') {\n throw new TypeError('executor must be a function.');\n }\n\n var resolvePromise;\n\n this.promise = new Promise(function promiseExecutor(resolve) {\n resolvePromise = resolve;\n });\n\n var token = this;\n\n // eslint-disable-next-line func-names\n this.promise.then(function(cancel) {\n if (!token._listeners) return;\n\n var i;\n var l = token._listeners.length;\n\n for (i = 0; i < l; i++) {\n token._listeners[i](cancel);\n }\n token._listeners = null;\n });\n\n // eslint-disable-next-line func-names\n this.promise.then = function(onfulfilled) {\n var _resolve;\n // eslint-disable-next-line func-names\n var promise = new Promise(function(resolve) {\n token.subscribe(resolve);\n _resolve = resolve;\n }).then(onfulfilled);\n\n promise.cancel = function reject() {\n token.unsubscribe(_resolve);\n };\n\n return promise;\n };\n\n executor(function cancel(message) {\n if (token.reason) {\n // Cancellation has already been requested\n return;\n }\n\n token.reason = new Cancel(message);\n resolvePromise(token.reason);\n });\n}\n\n/**\n * Throws a `Cancel` if cancellation has been requested.\n */\nCancelToken.prototype.throwIfRequested = function throwIfRequested() {\n if (this.reason) {\n throw this.reason;\n }\n};\n\n/**\n * Subscribe to the cancel signal\n */\n\nCancelToken.prototype.subscribe = function subscribe(listener) {\n if (this.reason) {\n listener(this.reason);\n return;\n }\n\n if (this._listeners) {\n this._listeners.push(listener);\n } else {\n this._listeners = [listener];\n }\n};\n\n/**\n * Unsubscribe from the cancel signal\n */\n\nCancelToken.prototype.unsubscribe = function unsubscribe(listener) {\n if (!this._listeners) {\n return;\n }\n var index = this._listeners.indexOf(listener);\n if (index !== -1) {\n this._listeners.splice(index, 1);\n }\n};\n\n/**\n * Returns an object that contains a new `CancelToken` and a function that, when called,\n * cancels the `CancelToken`.\n */\nCancelToken.source = function source() {\n var cancel;\n var token = new CancelToken(function executor(c) {\n cancel = c;\n });\n return {\n token: token,\n cancel: cancel\n };\n};\n\nmodule.exports = CancelToken;\n","'use strict';\n\n/**\n * Syntactic sugar for invoking a function and expanding an array for arguments.\n *\n * Common use case would be to use `Function.prototype.apply`.\n *\n * ```js\n * function f(x, y, z) {}\n * var args = [1, 2, 3];\n * f.apply(null, args);\n * ```\n *\n * With `spread` this example can be re-written.\n *\n * ```js\n * spread(function(x, y, z) {})([1, 2, 3]);\n * ```\n *\n * @param {Function} callback\n * @returns {Function}\n */\nmodule.exports = function spread(callback) {\n return function wrap(arr) {\n return callback.apply(null, arr);\n };\n};\n","'use strict';\n\n/**\n * Determines whether the payload is an error thrown by Axios\n *\n * @param {*} payload The value to test\n * @returns {boolean} True if the payload is an error thrown by Axios, otherwise false\n */\nmodule.exports = function isAxiosError(payload) {\n return (typeof payload === 'object') && (payload.isAxiosError === true);\n};\n","/** @license React v17.0.2\n * react-jsx-runtime.production.min.js\n *\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n'use strict';require(\"object-assign\");var f=require(\"react\"),g=60103;exports.Fragment=60107;if(\"function\"===typeof Symbol&&Symbol.for){var h=Symbol.for;g=h(\"react.element\");exports.Fragment=h(\"react.fragment\")}var m=f.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,n=Object.prototype.hasOwnProperty,p={key:!0,ref:!0,__self:!0,__source:!0};\nfunction q(c,a,k){var b,d={},e=null,l=null;void 0!==k&&(e=\"\"+k);void 0!==a.key&&(e=\"\"+a.key);void 0!==a.ref&&(l=a.ref);for(b in a)n.call(a,b)&&!p.hasOwnProperty(b)&&(d[b]=a[b]);if(c&&c.defaultProps)for(b in a=c.defaultProps,a)void 0===d[b]&&(d[b]=a[b]);return{$$typeof:g,type:c,key:e,ref:l,props:d,_owner:m.current}}exports.jsx=q;exports.jsxs=q;\n","function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }\n\nimport { CONTEXT_VERSION, LeafletProvider } from '@react-leaflet/core';\nimport { Map as LeafletMap } from 'leaflet';\nimport React, { useEffect, useMemo, useRef, useState } from 'react';\nexport function useMapElement(mapRef, props) {\n const [map, setMap] = useState(null);\n useEffect(() => {\n if (mapRef.current !== null && map === null) {\n const instance = new LeafletMap(mapRef.current, props);\n\n if (props.center != null && props.zoom != null) {\n instance.setView(props.center, props.zoom);\n } else if (props.bounds != null) {\n instance.fitBounds(props.bounds, props.boundsOptions);\n }\n\n if (props.whenReady != null) {\n instance.whenReady(props.whenReady);\n }\n\n setMap(instance);\n }\n }, [mapRef, map, props]);\n return map;\n}\nexport function MapContainer({\n children,\n className,\n id,\n placeholder,\n style,\n whenCreated,\n ...options\n}) {\n const mapRef = useRef(null);\n const map = useMapElement(mapRef, options);\n const createdRef = useRef(false);\n useEffect(() => {\n if (map != null && createdRef.current === false && whenCreated != null) {\n createdRef.current = true;\n whenCreated(map);\n }\n }, [map, whenCreated]);\n const [props] = useState({\n className,\n id,\n style\n });\n const context = useMemo(() => map ? {\n __version: CONTEXT_VERSION,\n map\n } : null, [map]);\n const contents = context ? /*#__PURE__*/React.createElement(LeafletProvider, {\n value: context\n }, children) : placeholder ?? null;\n return /*#__PURE__*/React.createElement(\"div\", _extends({}, props, {\n ref: mapRef\n }), contents);\n}","import { createLayerComponent } from '@react-leaflet/core';\nimport { Marker as LeafletMarker } from 'leaflet';\nexport const Marker = createLayerComponent(function createMarker({\n position,\n ...options\n}, ctx) {\n const instance = new LeafletMarker(position, options);\n return {\n instance,\n context: { ...ctx,\n overlayContainer: instance\n }\n };\n}, function updateMarker(marker, props, prevProps) {\n if (props.position !== prevProps.position) {\n marker.setLatLng(props.position);\n }\n\n if (props.icon != null && props.icon !== prevProps.icon) {\n marker.setIcon(props.icon);\n }\n\n if (props.zIndexOffset != null && props.zIndexOffset !== prevProps.zIndexOffset) {\n marker.setZIndexOffset(props.zIndexOffset);\n }\n\n if (props.opacity != null && props.opacity !== prevProps.opacity) {\n marker.setOpacity(props.opacity);\n }\n\n if (marker.dragging != null && props.draggable !== prevProps.draggable) {\n if (props.draggable === true) {\n marker.dragging.enable();\n } else {\n marker.dragging.disable();\n }\n }\n});","import { createOverlayComponent } from '@react-leaflet/core';\nimport { Popup as LeafletPopup } from 'leaflet';\nimport { useEffect } from 'react';\nexport const Popup = createOverlayComponent(function createPopup(props, context) {\n return {\n instance: new LeafletPopup(props, context.overlayContainer),\n context\n };\n}, function usePopupLifecycle(element, context, props, setOpen) {\n const {\n onClose,\n onOpen,\n position\n } = props;\n useEffect(function addPopup() {\n const {\n instance\n } = element;\n\n function onPopupOpen(event) {\n if (event.popup === instance) {\n instance.update();\n setOpen(true);\n onOpen == null ? void 0 : onOpen();\n }\n }\n\n function onPopupClose(event) {\n if (event.popup === instance) {\n setOpen(false);\n onClose == null ? void 0 : onClose();\n }\n }\n\n context.map.on({\n popupopen: onPopupOpen,\n popupclose: onPopupClose\n });\n\n if (context.overlayContainer == null) {\n // Attach to a Map\n if (position != null) {\n instance.setLatLng(position);\n }\n\n instance.openOn(context.map);\n } else {\n // Attach to container component\n context.overlayContainer.bindPopup(instance);\n }\n\n return function removePopup() {\n var _context$overlayConta;\n\n context.map.off({\n popupopen: onPopupOpen,\n popupclose: onPopupClose\n });\n (_context$overlayConta = context.overlayContainer) == null ? void 0 : _context$overlayConta.unbindPopup();\n context.map.removeLayer(instance);\n };\n }, [element, context, setOpen, onClose, onOpen, position]);\n});","import { createTileLayerComponent, updateGridLayer, withPane } from '@react-leaflet/core';\nimport { TileLayer as LeafletTileLayer } from 'leaflet';\nexport const TileLayer = createTileLayerComponent(function createTileLayer({\n url,\n ...options\n}, context) {\n return {\n instance: new LeafletTileLayer(url, withPane(options, context)),\n context\n };\n}, updateGridLayer);","export function updateGridLayer(layer, props, prevProps) {\n const {\n opacity,\n zIndex\n } = props;\n\n if (opacity != null && opacity !== prevProps.opacity) {\n layer.setOpacity(opacity);\n }\n\n if (zIndex != null && zIndex !== prevProps.zIndex) {\n layer.setZIndex(zIndex);\n }\n}"],"sourceRoot":""} \ No newline at end of file diff --git a/vendor/github.com/skycoin/skywire-services/pkg/node-visualizer/api/build/static/js/3.c05a463d.chunk.js b/vendor/github.com/skycoin/skywire-services/pkg/node-visualizer/api/build/static/js/3.c05a463d.chunk.js new file mode 100644 index 000000000..f1f5da014 --- /dev/null +++ b/vendor/github.com/skycoin/skywire-services/pkg/node-visualizer/api/build/static/js/3.c05a463d.chunk.js @@ -0,0 +1,2 @@ +(this.webpackJsonpweb=this.webpackJsonpweb||[]).push([[3],{81:function(t,e,n){"use strict";n.r(e),n.d(e,"getCLS",(function(){return d})),n.d(e,"getFCP",(function(){return S})),n.d(e,"getFID",(function(){return F})),n.d(e,"getLCP",(function(){return k})),n.d(e,"getTTFB",(function(){return C}));var i,a,r,o,u=function(t,e){return{name:t,value:void 0===e?-1:e,delta:0,entries:[],id:"v1-".concat(Date.now(),"-").concat(Math.floor(8999999999999*Math.random())+1e12)}},c=function(t,e){try{if(PerformanceObserver.supportedEntryTypes.includes(t)){if("first-input"===t&&!("PerformanceEventTiming"in self))return;var n=new PerformanceObserver((function(t){return t.getEntries().map(e)}));return n.observe({type:t,buffered:!0}),n}}catch(t){}},f=function(t,e){var n=function n(i){"pagehide"!==i.type&&"hidden"!==document.visibilityState||(t(i),e&&(removeEventListener("visibilitychange",n,!0),removeEventListener("pagehide",n,!0)))};addEventListener("visibilitychange",n,!0),addEventListener("pagehide",n,!0)},s=function(t){addEventListener("pageshow",(function(e){e.persisted&&t(e)}),!0)},m="function"==typeof WeakSet?new WeakSet:new Set,p=function(t,e,n){var i;return function(){e.value>=0&&(n||m.has(e)||"hidden"===document.visibilityState)&&(e.delta=e.value-(i||0),(e.delta||void 0===i)&&(i=e.value,t(e)))}},d=function(t,e){var n,i=u("CLS",0),a=function(t){t.hadRecentInput||(i.value+=t.value,i.entries.push(t),n())},r=c("layout-shift",a);r&&(n=p(t,i,e),f((function(){r.takeRecords().map(a),n()})),s((function(){i=u("CLS",0),n=p(t,i,e)})))},v=-1,l=function(){return"hidden"===document.visibilityState?0:1/0},h=function(){f((function(t){var e=t.timeStamp;v=e}),!0)},g=function(){return v<0&&(v=l(),h(),s((function(){setTimeout((function(){v=l(),h()}),0)}))),{get timeStamp(){return v}}},S=function(t,e){var n,i=g(),a=u("FCP"),r=function(t){"first-contentful-paint"===t.name&&(f&&f.disconnect(),t.startTime=0&&a1e12?new Date:performance.now())-t.timeStamp;"pointerdown"==t.type?function(t,e){var n=function(){E(t,e),a()},i=function(){a()},a=function(){removeEventListener("pointerup",n,y),removeEventListener("pointercancel",i,y)};addEventListener("pointerup",n,y),addEventListener("pointercancel",i,y)}(e,t):E(e,t)}},b=function(t){["mousedown","keydown","touchstart","pointerdown"].forEach((function(e){return t(e,T,y)}))},F=function(t,e){var n,r=g(),d=u("FID"),v=function(t){t.startTime=0&&(n||u.has(t)||\"hidden\"===document.visibilityState)&&(t.delta=t.value-(i||0),(t.delta||void 0===i)&&(i=t.value,e(t)))}},s=function(e,t){var n,i=a(\"CLS\",0),u=function(e){e.hadRecentInput||(i.value+=e.value,i.entries.push(e),n())},s=r(\"layout-shift\",u);s&&(n=f(e,i,t),o((function(){s.takeRecords().map(u),n()})),c((function(){i=a(\"CLS\",0),n=f(e,i,t)})))},m=-1,p=function(){return\"hidden\"===document.visibilityState?0:1/0},v=function(){o((function(e){var t=e.timeStamp;m=t}),!0)},d=function(){return m<0&&(m=p(),v(),c((function(){setTimeout((function(){m=p(),v()}),0)}))),{get timeStamp(){return m}}},l=function(e,t){var n,i=d(),o=a(\"FCP\"),s=function(e){\"first-contentful-paint\"===e.name&&(p&&p.disconnect(),e.startTime=0&&t1e12?new Date:performance.now())-e.timeStamp;\"pointerdown\"==e.type?function(e,t){var n=function(){y(e,t),a()},i=function(){a()},a=function(){removeEventListener(\"pointerup\",n,h),removeEventListener(\"pointercancel\",i,h)};addEventListener(\"pointerup\",n,h),addEventListener(\"pointercancel\",i,h)}(t,e):y(t,e)}},w=function(e){[\"mousedown\",\"keydown\",\"touchstart\",\"pointerdown\"].forEach((function(t){return e(t,E,h)}))},L=function(n,s){var m,p=d(),v=a(\"FID\"),l=function(e){e.startTime0?(S(t[0]),function(e){return a()}):function(e){}}};return Object(I.jsxs)(r.Fragment,{children:[Object(I.jsxs)(g.a,{zoom:3,center:[45,90],scrollWheelZoom:!1,bounds:new b.LatLngBounds(N,R),maxBounds:new b.LatLngBounds(N,R),children:[Object(I.jsx)(f.a,{url:"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"}),T.preprocessedResult.nodes.map((function(t){return Object(I.jsx)(O.a,{position:[t.x,t.y],eventHandlers:{click:function(e){var n=T.preprocessedResult.edges.filter(F);console.log("EDGES: ",n),E(Object(l.a)(Object(l.a)({},T),{},{selected:{node:t,edges:n}}))}},icon:e,children:Object(I.jsx)(h.a,{children:Object(I.jsxs)("div",{className:"map-popup",children:[Object(I.jsx)("h3",{children:"Visors"}),Object(I.jsx)("ul",{children:void 0!==t.public_keys&&t.public_keys.length>0?(n=t.public_keys,n.map((function(e,t){return Object(I.jsx)("li",{onClick:z(e),children:e})}))):Object(I.jsx)("p",{children:"Empty"})}),void 0!==_?Object(I.jsx)(v.a,{isOpen:c,onRequestClose:a,className:"edge-modal",overlayClassName:"edge-overlay",preventScroll:!1,children:Object(I.jsx)(K,{edge:_})}):Object(I.jsx)("div",{})]})})},t.id);var n}))]}),""!==T.errMsg?Object(y.a)({error:T.errMsg,position:"top-right",autoClose:5e3,closeOnClick:!0,pauseOnHover:!0,draggable:!0}):Object(I.jsx)("div",{}),T.loading?Object(y.a)({info:"loading data",position:"top-right",dismiss:T.loading}):Object(I.jsx)("div",{})]})};i.a.config();var B=function(){return Object(I.jsx)("div",{children:Object(I.jsx)(A,{})})},F=(n(74),function(e){e&&e instanceof Function&&n.e(3).then(n.bind(null,81)).then((function(t){var n=t.getCLS,r=t.getFID,c=t.getFCP,s=t.getLCP,a=t.getTTFB;n(e),r(e),c(e),s(e),a(e)}))});a.a.render(Object(I.jsx)(c.a.StrictMode,{children:Object(I.jsx)(B,{})}),document.getElementById("root")),F()}},[[75,1,2]]]); +//# sourceMappingURL=main.588f2c65.chunk.js.map \ No newline at end of file diff --git a/vendor/github.com/skycoin/skywire-services/pkg/node-visualizer/api/build/static/js/main.588f2c65.chunk.js.map b/vendor/github.com/skycoin/skywire-services/pkg/node-visualizer/api/build/static/js/main.588f2c65.chunk.js.map new file mode 100644 index 000000000..49489c433 --- /dev/null +++ b/vendor/github.com/skycoin/skywire-services/pkg/node-visualizer/api/build/static/js/main.588f2c65.chunk.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["images/marker.svg","utils/constants.ts","providers/httpClient.ts","providers/process.ts","components/graph/sidenavEdge.tsx","components/graph/graph.tsx","App.tsx","reportWebVitals.ts","index.tsx"],"names":["IP_API_URL","SKY_NODEVIZ_URL","process","httpClient","axios","create","headers","preprocessGraph","a","nodes","edges","Error","finalNodes","pkeyMap","Map","ips","Object","keys","getips","apiresult","key","undefined","node","id","title","x","latitude","y","longitude","public_keys","forEach","p_key","set","push","finalEdges","map","edge","handleTooltipText","sourcePKey","targetPKey","t_id","t_type","type","t_label","label","source","get","target","requests","chunk","rec","i","length","req","data","JSON","stringify","post","result","ipres","ip_address","arr","size","output","Array","Math","ceil","seekIndex","outputIndex","slice","fetchUptimePoll","then","r","SidenavEdge","className","Graph","markerIcon","Icon","iconUrl","marker","iconSize","useState","isOpen","setIsOpen","toggleModal","isInitial","setIsInitial","selectedEdge","setSelectedEdge","loading","errMsg","selected","preprocessedResult","graphState","setGraphState","fetchUpdate","res","e","filterEdges","index","array","j","useEffect","catch","interval","setInterval","clearInterval","southWest","LatLng","northEast","handleEdges","filter","el","idx","MapContainer","zoom","center","scrollWheelZoom","bounds","LatLngBounds","maxBounds","TileLayer","url","n","Marker","position","eventHandlers","click","_","selectedEdges","console","log","icon","Popup","pubkeys","k","onClick","onRequestClose","overlayClassName","preventScroll","toast","error","autoClose","closeOnClick","pauseOnHover","draggable","info","dismiss","dotenv","config","App","reportWebVitals","onPerfEntry","Function","getCLS","getFID","getFCP","getLCP","getTTFB","ReactDOM","render","StrictMode","document","getElementById"],"mappings":"qTAAe,MAA0B,mCCG5BA,EAAa,+BACbC,EAAkBC,4B,QCFlBC,E,OAAaC,EAAMC,OAAO,CACnCC,QAAS,CACL,8BAA+B,IAC/B,mCAAoC,OACpC,eAAgB,sBCMXC,EAAe,uCAAG,6CAAAC,EAAA,yDAAQC,EAAR,EAAQA,MAAOC,EAAf,EAAeA,MAC5B,OAAVD,GAA4B,OAAVC,EADK,sBAEjB,IAAIC,MAAM,4BAFO,cAKvBC,EAAyB,GACzBC,EAAU,IAAIC,IACdC,EAAMC,OAAOC,KAAKR,GAPK,SAQqBS,EAAOH,GAR5B,OAQvBI,EARuB,kBAUhBC,GACP,QAAoBC,IAAhBZ,EAAOW,GAAoB,iBAC/B,IAAIE,EAAO,CACPC,GAAIH,EACJI,MAAOJ,EACPK,EAAGN,EAAUC,GAAKM,SAClBC,EAAGR,EAAUC,GAAKQ,UAClBC,YAAapB,EAAOW,GAAKS,aAE7BpB,EAAOW,GAAKS,YAAYC,SAAQ,SAAAC,GAC5BlB,EAAQmB,IAAID,EAAOX,MAEvBR,EAAWqB,KAAKX,IAtBO,OAAAd,EAAA,KAUTW,GAVS,kDAUhBC,EAVgB,0BAUhBA,GAVgB,uFAyBvBc,EAAyBxB,EAAOyB,KAAI,SAAAC,GAWpC,MAVyB,CACrBC,kBAAkB,YAAD,OAAcD,EAAK1B,MAAM,GAAzB,wBAA2C0B,EAAK1B,MAAM,IACvE4B,WAAYF,EAAK1B,MAAM,GACvB6B,WAAYH,EAAK1B,MAAM,GACvB8B,KAAMJ,EAAKI,KACXC,OAAQL,EAAKM,KACbC,QAASP,EAAKQ,MACdC,OAAQhC,EAAQiC,IAAIV,EAAK1B,MAAM,IAC/BqC,OAAQlC,EAAQiC,IAAIV,EAAK1B,MAAM,QAlCZ,kBAsCpB,CACHD,MAAOG,EACPF,MAAOwB,EACPrB,QAASA,IAzCc,4CAAH,sD,SA6CbK,E,8EAAf,WAAsBH,GAAtB,uBAAAP,EAAA,sDACQwC,EAAWC,EAAMlC,EAAK,KACtBmC,EAAgC,GAG3BC,EAAI,EALjB,YAKoBA,EAAIH,EAASI,QALjC,wBAMYC,EAAoB,CACpBtC,IAAKiC,EAASG,IAEZG,EAAOC,KAAKC,UAAUH,GATpC,SAU0BlD,EAAWsD,KAAKzD,EAAYsD,GAVtD,cAWYA,KAAKI,OAAO5B,SAAQ,SAAC6B,GACrBT,EAAIS,EAAMC,YAAcD,KAZpC,QAKyCR,IALzC,gDAgBWD,GAhBX,6C,sBAmBA,SAASD,EAAMY,EAAeC,GAI1B,IAHA,IAAMV,EAASS,EAAIT,OACbW,EAAqB,IAAIC,MAAMC,KAAKC,KAAKd,EAASU,IACpDK,EAAY,EAAGC,EAAc,EAC1BD,EAAYf,GACfW,EAAOK,KAAiBP,EAAIQ,MAAMF,EAAWA,GAAaL,GAE9D,OAAOC,EAGJ,SAAeO,IAAtB,+B,4CAAO,4BAAA9D,EAAA,6DACCkD,EAAqB,GADtB,kBAGOvD,EAAW2C,IAAI7C,GAAiBsE,MAAK,SAACC,GACxCd,EAASc,EAAElB,QAJhB,gCAMQI,GANR,qG,iCClDQe,EA7BiC,SAAC,GAAc,IAAZrC,EAAW,EAAXA,KAC/C,OACI,sBAAKb,GAAG,YAAR,UACI,2CACA,mBAAGmD,UAAU,KAAb,SAAmBtC,EAAKI,OACxB,gDACCJ,EAAKS,OACN,uBACA,gDACCT,EAAKS,OACN,uBACA,gDACCT,EAAKW,OACN,uBACA,sDACCX,EAAKK,OACN,uBACA,sDACCL,EAAKO,QACN,uBACA,mDACA,4BAAIP,EAAKE,aACT,mDACA,4BAAIF,EAAKG,aACT,2BCwJGoC,EAnKqB,WAChC,IAAMC,EAAa,IAAIC,OAAK,CACxBC,QAASC,EACTC,SAAU,CAAC,GAAI,MAEnB,EAA4BC,oBAAS,GAArC,mBAAOC,EAAP,KAAeC,EAAf,KAEA,SAASC,IACLD,GAAWD,GAGf,MAAkCD,oBAAkB,GAApD,mBAAOI,EAAP,KAAkBC,EAAlB,KACA,EAAwCL,wBAA+B5D,GAAvE,mBAAOkE,EAAP,KAAqBC,EAArB,KACA,EAAoCP,mBAAqB,CACrDQ,SAAS,EACTC,OAAQ,GACRC,SAAU,CACNrE,UAAMD,EACNX,WAAOW,GAEXuE,mBAAoB,CAChBnF,MAAO,GACPC,MAAO,GACPG,QAAS,IAAIC,OAVrB,mBAAO+E,EAAP,KAAmBC,EAAnB,KAbsC,SA0CvBC,IA1CuB,2EA0CtC,sBAAAvF,EAAA,sDACIsF,EAAc,2BAAKD,GAAN,IAAkBJ,SAAS,KACxC,IACInB,IAAkBC,MAAK,SAACC,GACpBjE,EAAgBiE,GAAGD,MAAK,SAACyB,GACrBF,EAAc,2BAAKD,GAAN,IAAkBD,mBAAoBI,WAG7D,MAAOC,GACLH,EAAc,2BAAKD,GAAN,IAAkBH,OAAQO,EAAGR,SAAS,KAT3D,4CA1CsC,sBAuDtC,SAASS,EAAY9D,EAAgB+D,EAAeC,GAA6B,IAAD,EACxEnF,EAAI,UAAG4E,EAAWF,SAASrE,YAAvB,aAAG,EAA0BO,YACrC,QAAaR,IAATJ,EAAoB,OAAO,EAC/B,IAAK,IAAIoF,EAAI,EAAGA,EAAIpF,EAAKmC,OAAQiD,IAC7B,GAAIjE,EAAKE,aAAerB,EAAMoF,IAAMjE,EAAKG,aAAetB,EAAKoF,GACzD,OAAO,EAGf,OAAO,EAlCXC,qBAAU,WACNP,IAAcQ,OAAM,SAACN,GAAD,OAAOH,EAAc,2BAAKD,GAAN,IAAkBH,OAAQO,EAAGR,SAAS,QAC9EH,GAAa,KACd,IAEHgB,qBAAU,WACN,IAAME,EAAWC,aAAY,WACzBV,IAAcQ,OAAM,SAACN,GAAD,OAAOH,EAAc,2BAAKD,GAAN,IAAkBH,OAAQO,EAAGR,SAAS,UATnE,KAYf,OAAO,kBAAMiB,cAAcF,MAC5B,CAACnB,IA8BJ,IAAMsB,EAAY,IAAIC,UAAQ,IAAK,KAC7BC,EAAY,IAAID,SAAO,GAAI,KAE3BE,EAAc,SAAC1F,GACjB,QAAkCC,IAA9BwE,EAAWF,SAASjF,MAAxB,CACA,IAAI0B,EAAOyD,EAAWF,SAASjF,MAAOqG,QAAO,SAACC,EAAIC,EAAKpD,GAAV,OAAkBmD,EAAG1E,aAAelB,GAAO4F,EAAGzE,aAAenB,KAC1G,OAAIgB,EAAKgB,OAAS,GACdoC,EAAgBpD,EAAK,IACd,SAAC6D,GAAD,OAAYb,MACT,SAACa,OAOnB,OACI,eAAC,WAAD,WACI,eAACiB,EAAA,EAAD,CACIC,KAAM,EACNC,OAAQ,CAAC,GAAO,IAChBC,iBAAiB,EACjBC,OAAQ,IAAIC,eAAaZ,EAAWE,GACpCW,UAAW,IAAID,eAAaZ,EAAWE,GAL3C,UAOI,cAACY,EAAA,EAAD,CAAWC,IAAI,uDACd7B,EAAWD,mBAAmBnF,MAAM0B,KAAI,SAACwF,GAAD,OACrC,cAACC,EAAA,EAAD,CAEIC,SAAU,CAACF,EAAElG,EAAIkG,EAAEhG,GACnBmG,cAAe,CACXC,MAAO,SAACC,GACJ,IAAIC,EAA4BpC,EAAWD,mBAAmBlF,MAnCzEqG,OAAOb,GAoCIgC,QAAQC,IAAI,UAAWF,GACvBnC,EAAc,2BACPD,GADM,IAETF,SAAU,CACNrE,KAAMqG,EACNjH,MAAOuH,QAKvBG,KAAMxD,EAhBV,SAkBI,cAACyD,EAAA,EAAD,UACI,sBAAK3D,UAAU,YAAf,UACI,wCACA,kCACuBrD,IAAlBsG,EAAE9F,aAA6B8F,EAAE9F,YAAYuB,OAAS,GArCjEkF,EAsC6BX,EAAE9F,YArC1CyG,EAAQnG,KAAI,SAACoG,EAAWpF,GAAZ,OAA0B,oBAAIqF,QAAS1B,EAAYyB,GAAzB,SAA8BA,QAsCzC,6CAEQlH,IAAjBkE,EAEO,cAAC,IAAD,CACIL,OAAQA,EACRuD,eAAgBrD,EAChBV,UAAU,aACVgE,iBAAiB,eACjBC,eAAe,EALnB,SAOI,cAAC,EAAD,CAAavG,KAAMmD,MAGzB,8BArCToC,EAAEpG,IAhBN,IAAC+G,QA4DS,KAAtBzC,EAAWH,OACNkD,YAAM,CACJC,MAAOhD,EAAWH,OAClBmC,SAAU,YACViB,UAAW,IACXC,cAAc,EACdC,cAAc,EACdC,WAAW,IAEb,wBACLpD,EAAWJ,QACNmD,YAAM,CACJM,KAAM,eACNrB,SAAU,YACVsB,QAAStD,EAAWJ,UAEtB,4BC7KlB2D,IAAOC,SAIP,IAQeC,EARgC,WAC3C,OACI,8BACI,cAAC,EAAD,OCEGC,G,MAZS,SAACC,GACjBA,GAAeA,aAAuBC,UACtC,6BAAqBlF,MAAK,YAAkD,IAA/CmF,EAA8C,EAA9CA,OAAQC,EAAsC,EAAtCA,OAAQC,EAA8B,EAA9BA,OAAQC,EAAsB,EAAtBA,OAAQC,EAAc,EAAdA,QACzDJ,EAAOF,GACPG,EAAOH,GACPI,EAAOJ,GACPK,EAAOL,GACPM,EAAQN,QCFpBO,IAASC,OACL,cAAC,IAAMC,WAAP,UACI,cAAC,EAAD,MAEJC,SAASC,eAAe,SAM5BZ,M","file":"static/js/main.588f2c65.chunk.js","sourcesContent":["export default __webpack_public_path__ + \"static/media/marker.8a6e5ca6.svg\";","// export const IP_PRO_TOKEN = assertEnv(process.env.PRO_API_TOKEN, 'PRO_API_TOKEN')\n// export const IP_API_URL = \"http://ip-api.com/batch?fields=192\";\n// export const PRO_API_URL = `http://pro.ip-api.com/batch?key=${IP_PRO_TOKEN}`;\nexport const IP_API_URL = \"https://ip.skycoin.com/batch\";\nexport const SKY_NODEVIZ_URL = process.env.REACT_APP_SKY_NODEVIZ_URL || \"http://localhost:9081/map\";\n","import axios from \"axios\";\n\nexport const httpClient = axios.create({\n headers: {\n \"Access-Control-Allow-Origin\": \"*\",\n \"Access-Control-Allow-Credentials\": \"true\",\n \"Content-Type\": \"application/json\",\n },\n});\n","import {\n EdgeData,\n GeoIPRequest,\n GeoIPResponse,\n IPResult,\n NodeData,\n PollResult,\n PreProcessedGraphResult,\n} from \"../models/models\";\nimport {IP_API_URL, SKY_NODEVIZ_URL} from \"../utils/constants\";\nimport {httpClient} from \"./httpClient\";\n\nexport const preprocessGraph = async ({nodes, edges}: PollResult): Promise => {\n if (nodes === null || edges === null) {\n throw new Error(\"nodes and edges are null\");\n }\n\n let finalNodes: NodeData[] = [];\n let pkeyMap = new Map();\n let ips = Object.keys(nodes!);\n let apiresult: Record = await getips(ips);\n // Process Node and create a Map (Public Key -> IP)\n for (const key in apiresult) {\n if (nodes![key] === undefined) continue\n let node = {\n id: key,\n title: key,\n x: apiresult[key].latitude,\n y: apiresult[key].longitude,\n public_keys: nodes![key].public_keys,\n };\n nodes![key].public_keys.forEach(p_key => {\n pkeyMap.set(p_key, key);\n });\n finalNodes.push(node);\n }\n // Process Edges\n let finalEdges: EdgeData[] = edges!.map(edge => {\n let edgeData: EdgeData = {\n handleTooltipText: `Source : ${edge.edges[0]} \\n Target : ${edge.edges[1]}`,\n sourcePKey: edge.edges[0],\n targetPKey: edge.edges[1],\n t_id: edge.t_id,\n t_type: edge.type,\n t_label: edge.label,\n source: pkeyMap.get(edge.edges[0])!,\n target: pkeyMap.get(edge.edges[1])!,\n };\n return edgeData;\n });\n return {\n nodes: finalNodes,\n edges: finalEdges,\n pkeyMap: pkeyMap,\n };\n};\n\nasync function getips(ips: string[]): Promise> {\n let requests = chunk(ips, 300);\n let rec: Record = {};\n\n // chunk it\n for (let i = 0; i < requests.length; i++) {\n let req: GeoIPRequest = {\n ips: requests[i],\n };\n const data = JSON.stringify(req);\n const res = await httpClient.post(IP_API_URL, data);\n res.data.result.forEach((ipres: IPResult) => {\n rec[ipres.ip_address] = ipres;\n });\n }\n\n return rec;\n}\n\nfunction chunk(arr: string[], size: number): string[][] {\n const length = arr.length;\n const output: string[][] = new Array(Math.ceil(length / size));\n let seekIndex = 0, outputIndex = 0;\n while (seekIndex < length) {\n output[outputIndex++] = arr.slice(seekIndex, seekIndex += size);\n }\n return output;\n}\n\nexport async function fetchUptimePoll(): Promise {\n let result: PollResult = {};\n try {\n await httpClient.get(SKY_NODEVIZ_URL).then((r) => {\n result = r.data;\n });\n return result;\n } catch (e) {\n throw e;\n }\n}\n","import React from \"react\";\nimport { EdgeData } from \"../../models/models\";\n\nexport interface SidenavEdgeProps {\n edge: EdgeData;\n}\n\nconst SidenavEdge: React.FC = ({ edge }) => {\n return (\n
\n

Transport

\n

{edge.t_id}

\n Source IP:\n {edge.source}\n
\n Source IP:\n {edge.source}\n
\n Target IP:\n {edge.target}\n
\n Transport Type::\n {edge.t_type}\n
\n Transport Label:\n {edge.t_label}\n
\n Source Visor:\n

{edge.sourcePKey}

\n Target Visor:\n

{edge.targetPKey}

\n
\n
\n );\n};\n\nexport default SidenavEdge;\n","import { Icon, LatLng, LatLngBounds } from \"leaflet\";\nimport React, { Fragment, useEffect, useState } from \"react\";\nimport { MapContainer, Marker, Popup, TileLayer } from \"react-leaflet\";\nimport Modal from \"react-modal\";\nimport { toast } from \"react-toastify\";\nimport marker from \"../../images/marker.svg\";\nimport { EdgeData, NodeData, PreProcessedGraphResult, SelectedData } from \"../../models/models\";\nimport { fetchUptimePoll, preprocessGraph } from \"../../providers/process\";\nimport SidenavEdge from \"./sidenavEdge\";\n\nexport interface GraphProps {\n}\n\ninterface GraphState {\n loading: boolean;\n selected: SelectedData;\n preprocessedResult: PreProcessedGraphResult;\n errMsg: string;\n}\n\nconst Graph: React.FC = () => {\n const markerIcon = new Icon({\n iconUrl: marker,\n iconSize: [25, 25],\n });\n const [isOpen, setIsOpen] = useState(false);\n\n function toggleModal() {\n setIsOpen(!isOpen);\n }\n\n const [isInitial, setIsInitial] = useState(false);\n const [selectedEdge, setSelectedEdge] = useState(undefined);\n const [graphState, setGraphState] = useState({\n loading: false,\n errMsg: \"\",\n selected: {\n node: undefined,\n edges: undefined,\n },\n preprocessedResult: {\n nodes: [],\n edges: [],\n pkeyMap: new Map(),\n },\n });\n\n const TIMEOUT_MS = 60000 * 5;\n\n useEffect(() => {\n fetchUpdate().catch((e) => setGraphState({ ...graphState, errMsg: e, loading: false }));\n setIsInitial(true);\n }, []);\n\n useEffect(() => {\n const interval = setInterval(() => {\n fetchUpdate().catch((e) => setGraphState({ ...graphState, errMsg: e, loading: false }));\n }, TIMEOUT_MS);\n\n return () => clearInterval(interval);\n }, [isInitial]);\n\n async function fetchUpdate() {\n setGraphState({ ...graphState, loading: true });\n try {\n fetchUptimePoll().then((r) => {\n preprocessGraph(r).then((res) => {\n setGraphState({ ...graphState, preprocessedResult: res });\n });\n });\n } catch (e: any) {\n setGraphState({ ...graphState, errMsg: e, loading: false });\n }\n }\n\n function filterEdges(edge: EdgeData, index: number, array: EdgeData[]): boolean {\n let keys = graphState.selected.node?.public_keys;\n if (keys === undefined) return false;\n for (let j = 0; j < keys.length; j++) {\n if (edge.sourcePKey === keys![j] || edge.targetPKey === keys[j]) {\n return true;\n }\n }\n return false;\n }\n\n function getEdges(selected: NodeData, edges: EdgeData[]): EdgeData[] | undefined {\n return edges.filter(filterEdges);\n }\n\n const southWest = new LatLng(-90, -180);\n const northEast = new LatLng(90, 180);\n\n const handleEdges = (key: string) => {\n if (graphState.selected.edges === undefined) return;\n let edge = graphState.selected.edges!.filter((el, idx, arr) => el.sourcePKey === key || el.targetPKey === key);\n if (edge.length > 0) {\n setSelectedEdge(edge[0]);\n return (e: any) => toggleModal();\n } else return (e: any) => {};\n };\n\n const pubKeyRender = (pubkeys: string[]) => {\n return pubkeys.map((k: string, i: number) =>
  • {k}
  • );\n };\n\n return (\n \n \n \n {graphState.preprocessedResult.nodes.map((n) => (\n {\n let selectedEdges = getEdges(n, graphState.preprocessedResult.edges);\n console.log(\"EDGES: \", selectedEdges);\n setGraphState({\n ...graphState,\n selected: {\n node: n,\n edges: selectedEdges,\n },\n });\n },\n }}\n icon={markerIcon}\n >\n \n
    \n

    Visors

    \n
      \n {n.public_keys !== undefined && n.public_keys.length > 0\n ? pubKeyRender(n.public_keys)\n :

      Empty

      }\n
    \n {selectedEdge !== undefined\n ? (\n \n \n \n )\n :
    }\n
    \n {/**/}\n \n \n ))}\n \n {graphState.errMsg !== \"\"\n ? toast({\n error: graphState.errMsg,\n position: \"top-right\",\n autoClose: 5000,\n closeOnClick: true,\n pauseOnHover: true,\n draggable: true,\n })\n :
    }\n {graphState.loading\n ? toast({\n info: \"loading data\",\n position: \"top-right\",\n dismiss: graphState.loading,\n })\n :
    }\n \n );\n};\n\nexport default Graph;\n","import dotenv from \"dotenv\";\nimport React from \"react\";\nimport \"./App.css\";\nimport Graph from \"./components/graph/graph\";\n\ndotenv.config();\n\ninterface AppProps {}\n\nconst App: React.FunctionComponent = () => {\n return (\n
    \n \n
    \n );\n};\n\nexport default App;\n","import { ReportHandler } from \"web-vitals\";\n\nconst reportWebVitals = (onPerfEntry?: ReportHandler) => {\n if (onPerfEntry && onPerfEntry instanceof Function) {\n import(\"web-vitals\").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {\n getCLS(onPerfEntry);\n getFID(onPerfEntry);\n getFCP(onPerfEntry);\n getLCP(onPerfEntry);\n getTTFB(onPerfEntry);\n });\n }\n};\n\nexport default reportWebVitals;\n","import \"leaflet/dist/leaflet.css\";\nimport React from \"react\";\nimport ReactDOM from \"react-dom\";\nimport App from \"./App\";\nimport \"./index.css\";\nimport reportWebVitals from \"./reportWebVitals\";\n\nReactDOM.render(\n \n \n ,\n document.getElementById(\"root\"),\n);\n\n// If you want to start measuring performance in your app, pass a function\n// to log results (for example: reportWebVitals(console.log))\n// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals\nreportWebVitals();\n"],"sourceRoot":""} \ No newline at end of file diff --git a/vendor/github.com/skycoin/skywire-services/pkg/node-visualizer/api/build/static/js/runtime-main.21703e9e.js b/vendor/github.com/skycoin/skywire-services/pkg/node-visualizer/api/build/static/js/runtime-main.21703e9e.js new file mode 100644 index 000000000..d364cf8e9 --- /dev/null +++ b/vendor/github.com/skycoin/skywire-services/pkg/node-visualizer/api/build/static/js/runtime-main.21703e9e.js @@ -0,0 +1,2 @@ +!function(e){function r(r){for(var n,a,i=r[0],c=r[1],l=r[2],s=0,p=[];s