├── .dockerignore ├── .editorconfig ├── .gitattributes ├── .github ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── SECURITY.md ├── assets │ └── logo.svg ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── codeql.yml │ ├── go.yml │ └── release.yml ├── .gitignore ├── .gitpod.yml ├── .golangci.yml ├── .goreleaser.yml ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── Makefile ├── README.md ├── Taskfile.yaml ├── api └── get-latest.go ├── app ├── app.go ├── checker │ └── checker.go ├── config │ └── config.go ├── gh.go └── sync.go ├── cmd ├── factory │ └── default.go └── tran │ ├── help-topic.go │ ├── help.go │ └── root.go ├── constants ├── commands.go └── constants.go ├── core ├── crypt │ └── crypt.go ├── receiver │ ├── receive.go │ ├── receiver.go │ ├── state.go │ └── tranx-client.go ├── sender │ ├── handlers.go │ ├── sender.go │ ├── server.go │ ├── state.go │ ├── transfer.go │ └── tranx-client.go └── tranx │ ├── handlers.go │ ├── id.go │ ├── mailbox.go │ ├── routes.go │ └── server.go ├── data └── password-codes.go ├── dfs └── directory-file-system.go ├── docker ├── container │ └── Dockerfile ├── dev │ └── Dockerfile └── vm │ └── Dockerfile ├── go.mod ├── go.sum ├── internal ├── config │ └── config.go ├── theme │ └── theme.go └── tui │ ├── cmds.go │ ├── init.go │ ├── keymap.go │ ├── model.go │ ├── receive.go │ ├── receiver.go │ ├── send.go │ ├── sender.go │ ├── sr-ui.go │ ├── update.go │ └── view.go ├── ios ├── color.go ├── console.go ├── console_windows.go ├── iostreams.go ├── tty_size.go └── tty_size_windows.go ├── main.go ├── models ├── model.go └── protocol │ ├── transfer.go │ └── tranx.go ├── renderer └── renderer.go ├── schema.json ├── scripts ├── bfs.ps1 ├── date.go ├── gh-tran │ ├── gh-trn.js │ ├── package.json │ ├── templates │ │ └── gh-tran │ └── yarn.lock ├── install.ps1 ├── install.sh ├── shell │ ├── README │ └── zshrc └── tag.sh └── tools ├── errors.go ├── files.go ├── json.go ├── math.go ├── messaging.go ├── password.go ├── ports.go ├── strings.go ├── text.go └── websocket.go /.dockerignore: -------------------------------------------------------------------------------- 1 | # build dirs 2 | dist 3 | 4 | # tran files 5 | *.exe 6 | *.exe~ 7 | tran 8 | 9 | # dependency directories 10 | vendor 11 | node_modules 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; http://editorconfig.org/ 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | 11 | [*.go] 12 | indent_style = tab 13 | indent_size = 4 14 | 15 | [*.{json,md}] 16 | indent_style = space 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sh text eol=lf 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @abdfnx -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | abdfn@secman.dev. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 0.1.x | :white_check_mark: | 11 | | < 0.1 | :x: | 12 | 13 | ## Reporting a Vulnerability 14 | 15 | Use this section to tell people how to report a vulnerability. 16 | 17 | Tell them where to go, how often they can expect to get an update on a 18 | reported vulnerability, what to expect if the vulnerability is accepted or 19 | declined, etc. 20 | -------------------------------------------------------------------------------- /.github/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: "gomod" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | reviewers: 9 | - abdfnx 10 | - package-ecosystem: npm 11 | directory: "/scripts/gh-tran" 12 | schedule: 13 | interval: "daily" 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Tran CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | GITHUB_TOKEN: ${{ github.token }} 11 | ACTIONS_ALLOW_UNSECURE_COMMANDS: true 12 | 13 | jobs: 14 | bfs: # build from source 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - name: Set up Go 21 | uses: actions/setup-go@v2 22 | with: 23 | go-version: 1.18 24 | 25 | - name: Building From Source 26 | run: | 27 | go mod tidy -compat=1.18 28 | go run ./scripts/date.go >> date.txt 29 | go build -ldflags "-X main.version=$(git describe --abbrev=0 --tags) -X main.buildDate=$(cat date.txt)" -o tran 30 | 31 | - name: Run Help 32 | run: ./tran help 33 | 34 | bfs_windows: # build from source (windows) 35 | runs-on: windows-latest 36 | 37 | steps: 38 | - uses: actions/checkout@v2 39 | 40 | - name: Set up Go 41 | uses: actions/setup-go@v2 42 | with: 43 | go-version: 1.18 44 | 45 | - name: Building From Source 46 | run: | 47 | .\scripts\bfs.ps1 48 | echo "::add-path::C:\Users\runneradmin\AppData\Local\tran\bin\;" 49 | 50 | - name: Run Help 51 | run: tran help 52 | 53 | from_script: 54 | needs: [ bfs ] 55 | 56 | runs-on: ubuntu-latest 57 | 58 | steps: 59 | - uses: actions/checkout@v2 60 | 61 | - name: Install from script 62 | run: curl -sL https://cutt.ly/tran-cli | bash 63 | 64 | - name: Run Help 65 | run: tran help 66 | 67 | from_script_windows: 68 | needs: [ bfs_windows ] 69 | 70 | runs-on: windows-latest 71 | 72 | steps: 73 | - uses: actions/checkout@v2 74 | 75 | - name: Install from script 76 | run: | 77 | iwr -useb https://cutt.ly/tran-win | iex 78 | echo "::add-path::C:\Users\runneradmin\AppData\Local\tran\bin\;" 79 | 80 | - name: Run Help 81 | run: tran help 82 | 83 | snapshot: 84 | needs: [ bfs, bfs_windows ] 85 | 86 | runs-on: ubuntu-latest 87 | 88 | steps: 89 | - uses: actions/checkout@v2 90 | 91 | - name: Set up Go 92 | uses: actions/setup-go@v2 93 | with: 94 | go-version: 1.18 95 | 96 | - name: Set up `GoReleaser` 97 | uses: goreleaser/goreleaser-action@v2 98 | with: 99 | install-only: true 100 | 101 | - name: Set up `Date` 102 | run: go run ./scripts/date.go >> date.txt 103 | 104 | - name: Build 105 | run: BuildDate="$(cat date.txt)" goreleaser release --snapshot --rm-dist --timeout 100m 106 | 107 | homebrew: 108 | needs: [ bfs, snapshot ] 109 | 110 | runs-on: macos-latest 111 | 112 | steps: 113 | - uses: actions/checkout@v2 114 | 115 | - name: Get Tran via homebrew 116 | run: brew install abdfnx/tap/tran 117 | 118 | - name: Run `tran help` 119 | run: tran help 120 | 121 | # via_docker: 122 | # needs: [ bfs, from_script, go ] 123 | 124 | # runs-on: ubuntu-latest 125 | 126 | # steps: 127 | # - uses: actions/checkout@v2 128 | 129 | # - name: Run in docker container 130 | # run: docker run --rm -iv trancli/tran -h 131 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | schedule: 9 | - cron: '40 8 * * 0' 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ 'go', 'javascript' ] 24 | 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v2 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v1 31 | with: 32 | languages: ${{ matrix.language }} 33 | 34 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 35 | # If this step fails, then you should remove it and run the build manually (see below) 36 | - name: Autobuild 37 | uses: github/codeql-action/autobuild@v1 38 | 39 | # ℹ️ Command-line programs to run using the OS shell. 40 | # 📚 https://git.io/JvXDl 41 | 42 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 43 | # and modify them (or add more) to build your code if your project 44 | # uses a compiled language 45 | 46 | - name: Perform CodeQL Analysis 47 | uses: github/codeql-action/analyze@v1 48 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Set up Go 16 | uses: actions/setup-go@v2 17 | with: 18 | go-version: 1.18 19 | 20 | - name: Build 21 | run: | 22 | go run ./scripts/date.go >> date.txt 23 | go build -ldflags "-X main.version=$(git describe --abbrev=0 --tags) -X main.buildDate=$(cat date.txt)" -o tran 24 | 25 | - name: Test 26 | run: go test -v ./... 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | env: 9 | GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} 10 | GITHUB_ACTIONS_NAME: "github-actions[bot]" 11 | GITHUB_ACTIONS_EMAIL: "41898282+github-actions[bot]@users.noreply.github.com" 12 | 13 | permissions: write-all 14 | 15 | jobs: 16 | build_tran: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | with: 22 | persist-credentials: false 23 | fetch-depth: 0 24 | 25 | - name: Set up Go 26 | uses: actions/setup-go@v2 27 | with: 28 | go-version: 1.18 29 | 30 | - name: Setup Node.js 31 | uses: actions/setup-node@v2.5.0 32 | with: 33 | node-version: 16 34 | 35 | - name: Set up GoReleaser 36 | uses: goreleaser/goreleaser-action@v2 37 | with: 38 | install-only: true 39 | 40 | - name: Set up Task 41 | uses: arduino/setup-task@v1 42 | 43 | - name: Set up Tag 44 | id: ghtag 45 | run: echo ::set-output name=tag::${GITHUB_REF#refs/*/} 46 | 47 | - name: Set up Date 48 | run: go run ./scripts/date.go >> date.txt 49 | 50 | - name: Build 51 | run: BuildDate="$(cat date.txt)" goreleaser release --rm-dist --timeout 100m 52 | 53 | - name: Build `gh-tran` 54 | env: 55 | TAG: ${{ steps.ghtag.outputs.tag }} 56 | run: | 57 | DATE="$(cat date.txt)" 58 | gh repo clone abdfnx/gh-tran 59 | cd gh-tran 60 | ./release.sh $TAG $DATE 61 | cd ../scripts/gh-tran 62 | yarn 63 | cd ../.. 64 | task ght 65 | 66 | - name: Commit files 67 | env: 68 | TAG: ${{ steps.ghtag.outputs.tag }} 69 | run: | 70 | cd ./scripts/gh-tran/tmp/gh-tran 71 | git config --local user.email "${{ env.GITHUB_ACTIONS_EMAIL }}" 72 | git config --local user.name "${{ env.GITHUB_ACTIONS_NAME }}" 73 | git diff --cached 74 | git add . 75 | git commit -m "tran ${TAG}" 76 | 77 | - name: Push changes 78 | uses: ad-m/github-push-action@master 79 | with: 80 | repository: "abdfnx/gh-tran" 81 | github_token: ${{ secrets.ACCESS_TOKEN }} 82 | directory: ./scripts/gh-tran/tmp/gh-tran 83 | 84 | - name: Login to Docker Hub 85 | uses: docker/login-action@v1 86 | with: 87 | username: ${{ secrets.DOCKER_ID }} 88 | password: ${{ secrets.DOCKER_ACCESS_TOKEN }} 89 | 90 | - name: Build tran from Docker 91 | run: | 92 | task build 93 | cp tran ./docker/vm 94 | 95 | - name: Build Tran Containers 96 | run: | 97 | task build-tran-container 98 | task build-tran-full-container 99 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | /tran 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | vendor 17 | node_modules 18 | 19 | # Build Files 20 | date.txt 21 | tag.txt 22 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: go get -d ./... 3 | command: | 4 | brew install go-task/tap/go-task 5 | curl -fsSL https://cutt.ly/tran-cli | bash 6 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | [ gofmt ] 4 | 5 | issues: 6 | max-issues-per-linter: 0 7 | max-same-issues: 0 8 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: tran 2 | 3 | env: 4 | - CGO_ENABLED=0 5 | 6 | release: 7 | prerelease: auto 8 | draft: true 9 | name_template: "Tran 💻 v{{ .Version }}" 10 | 11 | before: 12 | hooks: 13 | - go mod tidy -compat=1.18 14 | 15 | builds: 16 | - <<: &build_defaults 17 | binary: bin/tran 18 | main: ./ 19 | ldflags: 20 | - -X main.version=v{{ .Version }} 21 | - -X main.buildDate={{ .Env.BuildDate }} 22 | 23 | id: macos 24 | goos: [ darwin ] 25 | goarch: [ amd64, arm64, arm ] 26 | 27 | - <<: *build_defaults 28 | id: linux 29 | goos: [ linux ] 30 | goarch: [ amd64, arm64, arm, 386 ] 31 | 32 | - <<: *build_defaults 33 | id: windows 34 | goos: [ windows ] 35 | goarch: [ amd64, arm64, arm, 386 ] 36 | 37 | - <<: *build_defaults 38 | id: freebsd 39 | goos: [ freebsd ] 40 | goarch: [ amd64, arm64, arm, 386 ] 41 | 42 | archives: 43 | - id: nix 44 | builds: [ macos, linux, freebsd ] 45 | <<: &archive_defaults 46 | name_template: "{{ .ProjectName }}_{{ .Os }}_v{{ .Version }}_{{ .Arch }}" 47 | 48 | wrap_in_directory: "true" 49 | replacements: 50 | darwin: macos 51 | format: zip 52 | files: 53 | - LICENSE 54 | 55 | - id: windows 56 | builds: [ windows ] 57 | <<: *archive_defaults 58 | wrap_in_directory: "false" 59 | format: zip 60 | files: 61 | - LICENSE 62 | 63 | nfpms: 64 | - license: MIT 65 | maintainer: abdfnx 66 | homepage: https://github.com/abdfnx/tran 67 | bindir: /usr 68 | file_name_template: "{{ .ProjectName }}_v{{ .Version }}_{{ .Arch }}" 69 | description: "🖥️ Securely transfer and send anything between computers with TUI" 70 | formats: 71 | - apk 72 | - deb 73 | - rpm 74 | 75 | brews: 76 | - goarm: 6 77 | tap: 78 | owner: abdfnx 79 | name: homebrew-tap 80 | homepage: "https://github.com/abdfnx/tran" 81 | description: "🖥️ Securely transfer and send anything between computers with TUI" 82 | 83 | checksum: 84 | name_template: "checksums.txt" 85 | 86 | snapshot: 87 | name_template: "{{ .Tag }}-next" 88 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "golang.go", 4 | "ms-azuretools.vscode-docker", 5 | "esbenp.prettier-vscode", 6 | "PKief.material-icon-theme", 7 | "aaron-bond.better-comments" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "vendor/**": true 4 | }, 5 | "editor.formatOnSave": true, 6 | "editor.fontLigatures": true, 7 | "git.autofetch": true, 8 | "git.confirmSync": false, 9 | "editor.defaultFormatter": "esbenp.prettier-vscode", 10 | "[go]": { 11 | "editor.formatOnSave": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Abdfn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | 3 | TAG=$(shell git describe --abbrev=0 --tags) 4 | DATE=$(shell go run ./scripts/date.go) 5 | 6 | build: 7 | @go mod tidy && \ 8 | go build -ldflags "-X main.version=$(TAG) -X main.buildDate=$(DATE)" -o tran 9 | 10 | install: tran 11 | @mv tran /usr/local/bin 12 | 13 | jbtc: # just build tran container without pushing it 14 | @docker build --file ./docker/vm/Dockerfile -t trancli/tran . 15 | 16 | btc: # build tran container 17 | @docker push trancli/tran 18 | 19 | btcwc: # build tran container with cache 20 | @docker pull trancli/tran:latest && \ 21 | docker build -t trancli/tran --cache-from trancli/tran:latest . && \ 22 | docker push trancli/tran 23 | 24 | jbftc: # just build full tran container without pushing it 25 | @docker build --file ./docker/container/Dockerfile -t trancli/tran-full . 26 | 27 | bftc: # build full tran container 28 | @docker push trancli/tran-full 29 | 30 | bftcwc: # build full tran container with cache 31 | @docker pull trancli/tran-full:latest && \ 32 | docker build -t trancli/tran-full --cache-from trancli/tran-full:latest . && \ 33 | docker push trancli/tran-full 34 | 35 | ght: 36 | @node ./scripts/gh-tran/gh-trn.js 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | 7 |

8 | 9 |

10 | 11 |

12 | 13 | > 🖥️ Securely transfer and send anything between computers with TUI. 14 | 15 | ## Installation 16 | 17 | ### Using script 18 | 19 | * Shell 20 | 21 | ``` 22 | curl -fsSL https://cutt.ly/tran-cli | bash 23 | ``` 24 | 25 | * PowerShell 26 | 27 | ``` 28 | iwr -useb https://cutt.ly/tran-win | iex 29 | ``` 30 | 31 | **then restart your powershell** 32 | 33 | ### Homebrew 34 | 35 | ```bash 36 | brew install abdfnx/tap/tran 37 | ``` 38 | 39 | ### GitHub CLI 40 | 41 | ```bash 42 | gh extension install abdfnx/gh-tran 43 | ``` 44 | 45 | ## Usage 46 | 47 | * Open Tran UI 48 | 49 | ```bash 50 | tran 51 | ``` 52 | 53 | * Open with specific path 54 | 55 | ``` 56 | tran --start-dir $PATH 57 | ``` 58 | 59 | * Send files to a remote computer 60 | 61 | ``` 62 | tran send 63 | ``` 64 | 65 | * Receive files from a remote computer 66 | 67 | ``` 68 | tran receive 69 | ``` 70 | 71 | * Authenticate with github 72 | 73 | ``` 74 | tran auth login 75 | ``` 76 | 77 | * Sync your tran config file 78 | 79 | ``` 80 | tran sync start 81 | ``` 82 | 83 | ### Tran Config file 84 | 85 | > tran config file is located at `~/.tran/tran.yml` 86 | 87 | ```yml 88 | config: 89 | borderless: false 90 | editor: vim 91 | enable_mousewheel: true 92 | show_updates: true 93 | start_dir: . 94 | ``` 95 | 96 | ### Flags 97 | 98 | ``` 99 | --start-dir string Starting directory for Tran 100 | ``` 101 | 102 | ### Shortkeys 103 | 104 | * tab: Switch between boxes 105 | * up: Move up 106 | * down: Move down 107 | * left: Go back a directory 108 | * right: Read file or enter directory 109 | * V: View directory 110 | * T: Go to top 111 | * G: Go to bottom 112 | * ~: Go to your home directory 113 | * /: Go to root directory 114 | * .: Toggle hidden files and directories 115 | * D: Only show directories 116 | * F: Only show files 117 | * E: Edit file 118 | * ctrl+s: Send files/directories to remote 119 | * ctrl+r: Receive files/directories from remote 120 | * ctrl+f: Find files and directories by name 121 | * q/ctrl+q: Quit 122 | 123 | ### Technologies Used in Tran 124 | 125 | - [**Charm**](https://charm.sh) 126 | - [**Chroma**](https://github.com/alecthomas/chroma) 127 | - [**Imaging**](https://github.com/disintegration/imaging) 128 | - [**Gorilla Websocket**](https://github.com/gorilla/websocket) 129 | - [**PAKE**](https://github.com/schollz/pake) 130 | - [**Cobra**](https://github.com/spf13/cobra) 131 | - [**Viper**](https://github.com/spf13/viper) 132 | - [**GJson**](https://github.com/tidwall/gjson) 133 | - [**Termenv**](https://github.com/muesli/termenv) 134 | 135 | ### Special thanks 136 | 137 | thanks to [**@ZinoKader**](https://github.com/ZinoKader) for his awesome repo [`portal`](https://github.com/ZinoKader/portal) 138 | 139 | ### License 140 | 141 | tran is licensed under the terms of [MIT](https://github.com/abdfnx/tran/blob/main/LICENSE) license. 142 | 143 | 144 | ## Stargazers over time 145 | 146 | [![Stargazers over time](https://starchart.cc/abdfnx/tran.svg)](https://starchart.cc/abdfnx/tran) 147 | -------------------------------------------------------------------------------- /Taskfile.yaml: -------------------------------------------------------------------------------- 1 | # https://taskfile.dev 2 | 3 | version: "3" 4 | 5 | vars: 6 | TRAN_CONTAINER: trancli/tran 7 | TRAN_FULL_CONTAINER: trancli/tran-full 8 | 9 | tasks: 10 | default: 11 | deps: [ build, ght ] 12 | 13 | set-tag-and-date: 14 | cmds: 15 | - if [ -f "date.txt" ]; then rm date.txt; fi 16 | - if [ -f "tag.txt" ]; then rm tag.txt; fi 17 | - go run ./scripts/date.go >> date.txt 18 | - git describe --abbrev=0 --tags >> tag.txt 19 | 20 | build: 21 | cmds: 22 | - task: set-tag-and-date 23 | - go get -d 24 | # - go get -u 25 | - go build -ldflags "-X main.version=$(cat tag.txt) -X main.buildDate=$(cat date.txt)" -o tran 26 | 27 | install: 28 | deps: [ build ] 29 | cmds: 30 | - sudo mv tran /usr/local/bin 31 | 32 | remove: 33 | cmds: 34 | - sudo rm -rf /usr/local/bin/tran 35 | 36 | tran-container: 37 | deps: [ just-build-tran-container, build-tran-container, build-tran-container-with-cache ] 38 | 39 | tran-full-container: 40 | deps: [ just-build-tran-full-container, build-tran-full-container, build-full-tran-container-with-cache ] 41 | 42 | just-build-tran-container: 43 | dir: ./docker/vm 44 | cmds: 45 | - docker build -t "{{ .TRAN_CONTAINER }}" . 46 | 47 | build-tran-container: 48 | deps: [ just-build-tran-container ] 49 | dir: ./docker/vm 50 | cmds: 51 | - docker push "{{ .TRAN_CONTAINER }}" 52 | 53 | build-tran-container-with-cache: 54 | cmds: 55 | - docker pull "{{ .TRAN_CONTAINER }}":latest 56 | - docker build -t "{{ .TRAN_CONTAINER }}" --cache-from "{{ .TRAN_CONTAINER }}":latest . 57 | - docker push "{{ .TRAN_CONTAINER }}" 58 | 59 | just-build-tran-full-container: 60 | dir: ./docker/container 61 | cmds: 62 | - docker build -t "{{ .TRAN_CONTAINER }}" . 63 | 64 | build-tran-full-container: 65 | deps: [ just-build-tran-full-container ] 66 | dir: ./docker/container 67 | cmds: 68 | - docker push "{{ .TRAN_CONTAINER }}" 69 | 70 | build-full-tran-container-with-cache: 71 | cmds: 72 | - docker pull "{{ .TRAN_CONTAINER }}":latest && \ 73 | - docker build -t "{{ .TRAN_CONTAINER }}" --cache-from "{{ .TRAN_CONTAINER }}":latest . && \ 74 | - docker push "{{ .TRAN_CONTAINER }}" 75 | 76 | check_node_moduels: 77 | dir: ./scripts/gh-tran 78 | cmds: 79 | - if ! [ -d "node_modules" ]; then yarn; fi 80 | 81 | ght: 82 | deps: [ build ] 83 | cmds: 84 | - task: check_node_moduels 85 | - node ./scripts/gh-tran/gh-trn.js 86 | -------------------------------------------------------------------------------- /api/get-latest.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | "net/http" 7 | "io/ioutil" 8 | 9 | "github.com/tidwall/gjson" 10 | "github.com/briandowns/spinner" 11 | httpClient "github.com/abdfnx/resto/client" 12 | ) 13 | 14 | func GetLatest() string { 15 | url := "https://api.github.com/repos/abdfnx/tran/releases/latest" 16 | 17 | req, err := http.NewRequest("GET", url, nil) 18 | 19 | if err != nil { 20 | fmt.Errorf("Error creating request: %s", err.Error()) 21 | } 22 | 23 | s := spinner.New(spinner.CharSets[11], 100*time.Millisecond) 24 | s.Suffix = " 🔍 Checking for updates..." 25 | s.Start() 26 | 27 | client := httpClient.HttpClient() 28 | res, err := client.Do(req) 29 | 30 | if err != nil { 31 | fmt.Printf("Error sending request: %s", err.Error()) 32 | } 33 | 34 | defer res.Body.Close() 35 | 36 | b, err := ioutil.ReadAll(res.Body) 37 | 38 | if err != nil { 39 | fmt.Printf("Error reading response: %s", err.Error()) 40 | } 41 | 42 | body := string(b) 43 | 44 | tag_name := gjson.Get(body, "tag_name") 45 | 46 | latestVersion := tag_name.String() 47 | 48 | s.Stop() 49 | 50 | return latestVersion 51 | } 52 | -------------------------------------------------------------------------------- /app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/abdfnx/tran/tools" 8 | "github.com/abdfnx/tran/models" 9 | "github.com/abdfnx/tran/constants" 10 | "github.com/abdfnx/tran/internal/tui" 11 | "github.com/abdfnx/gh/pkg/cmd/factory" 12 | ) 13 | 14 | var NewSendCmd = &cobra.Command{ 15 | Use: "send", 16 | Short: "Send files/directories to remote", 17 | Long: "Send files/directories to remote", 18 | RunE: func(cmd *cobra.Command, args []string) error { 19 | tools.RandomSeed() 20 | 21 | err := tui.ValidateTranxAddress() 22 | 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | 27 | tui.HandleSendCommand(models.TranOptions{ 28 | TranxAddress: constants.DEFAULT_ADDRESS, 29 | TranxPort: constants.DEFAULT_PORT, 30 | }, args) 31 | 32 | return nil 33 | }, 34 | } 35 | 36 | var NewReceiveCmd = &cobra.Command{ 37 | Use: "receive", 38 | Short: "Receive files/directories from remote", 39 | Long: "Receive files/directories from remote", 40 | RunE: func(cmd *cobra.Command, args []string) error { 41 | err := tui.ValidateTranxAddress() 42 | 43 | if err != nil { 44 | return err 45 | } 46 | 47 | tui.HandleReceiveCommand(models.TranOptions{ 48 | TranxAddress: constants.DEFAULT_ADDRESS, 49 | TranxPort: constants.DEFAULT_PORT, 50 | }, args[0]) 51 | 52 | return nil 53 | }, 54 | } 55 | 56 | var NewAuthCmd = Auth(factory.New()) 57 | var NewGHConfigCmd = GHConfig(factory.New()) 58 | var NewGHRepoCmd = Repo(factory.New()) 59 | -------------------------------------------------------------------------------- /app/checker/checker.go: -------------------------------------------------------------------------------- 1 | package checker 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/mgutz/ansi" 8 | "github.com/abdfnx/looker" 9 | "github.com/abdfnx/tran/api" 10 | "github.com/abdfnx/tran/cmd/factory" 11 | "github.com/abdfnx/tran/internal/config" 12 | ) 13 | 14 | func Check(buildVersion string) { 15 | cmdFactory := factory.New() 16 | stderr := cmdFactory.IOStreams.ErrOut 17 | cfg := config.GetConfig() 18 | 19 | latestVersion := api.GetLatest() 20 | isFromHomebrewTap := isUnderHomebrew() 21 | isFromUsrBinDir := isUnderUsr() 22 | isFromGHCLI := isUnderGHCLI() 23 | isFromAppData := isUnderAppData() 24 | 25 | var command = func() string { 26 | if isFromHomebrewTap { 27 | return "brew upgrade tran" 28 | } else if isFromUsrBinDir { 29 | return "curl -fsSL https://cutt.ly/tran-cli | bash" 30 | } else if isFromGHCLI { 31 | return "gh extention upgrade tran" 32 | } else if isFromAppData { 33 | return "iwr -useb https://cutt.ly/tran-win | iex" 34 | } 35 | 36 | return "" 37 | } 38 | 39 | if buildVersion != latestVersion && cfg.Tran.ShowUpdates { 40 | fmt.Fprintf(stderr, "%s %s → %s\n", 41 | ansi.Color("There's a new version of ", "yellow") + ansi.Color("tran", "cyan") + ansi.Color(" is avalaible:", "yellow"), 42 | ansi.Color(buildVersion, "cyan"), 43 | ansi.Color(latestVersion, "cyan")) 44 | 45 | if command() != "" { 46 | fmt.Fprintf(stderr, ansi.Color("To upgrade, run: %s\n", "yellow"), ansi.Color(command(), "black:white")) 47 | } 48 | } 49 | } 50 | 51 | var tranExe, _ = looker.LookPath("tran") 52 | 53 | func isUnderHomebrew() bool { 54 | return strings.Contains(tranExe, "brew") 55 | } 56 | 57 | func isUnderUsr() bool { 58 | return strings.Contains(tranExe, "usr") 59 | } 60 | 61 | func isUnderAppData() bool { 62 | return strings.Contains(tranExe, "AppData") 63 | } 64 | 65 | func isUnderGHCLI() bool { 66 | return strings.Contains(tranExe, "gh") 67 | } 68 | -------------------------------------------------------------------------------- /app/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "bytes" 6 | "io/ioutil" 7 | "path/filepath" 8 | 9 | "github.com/mgutz/ansi" 10 | "github.com/spf13/cobra" 11 | "github.com/spf13/viper" 12 | "github.com/abdfnx/tran/dfs" 13 | ) 14 | 15 | var ( 16 | homeDir, _ = dfs.GetHomeDirectory() 17 | tranConfigPath = filepath.Join(homeDir, ".tran", "tran.yml") 18 | tranConfig, err = ioutil.ReadFile(tranConfigPath) 19 | ) 20 | 21 | func NewConfigCmd() *cobra.Command { 22 | cmd := &cobra.Command{ 23 | Use: "config", 24 | Short: "Configure tran", 25 | Long: "Configure tran, including setting up tran editor, etc.", 26 | RunE: func(cmd *cobra.Command, args []string) error { 27 | return nil 28 | }, 29 | } 30 | 31 | cmd.AddCommand(NewConfigSetCmd) 32 | cmd.AddCommand(NewConfigGetCmd) 33 | cmd.AddCommand(NewConfigListCmd) 34 | 35 | return cmd 36 | } 37 | 38 | var NewConfigSetCmd = &cobra.Command{ 39 | Use: "set", 40 | Short: "Update tran configuration", 41 | Long: "Update tran configuration, such as editor, show updates, etc.", 42 | RunE: func(cmd *cobra.Command, args []string) error { 43 | if err != nil { 44 | return err 45 | } 46 | 47 | viper.SetConfigType("yaml") 48 | 49 | viper.ReadConfig(bytes.NewBuffer(tranConfig)) 50 | 51 | // set new key value but keep existing values 52 | viper.Set("config." + args[0], args[1]) 53 | 54 | // write config to file 55 | err := viper.WriteConfigAs(tranConfigPath) 56 | 57 | if err != nil { 58 | return err 59 | } 60 | 61 | fmt.Println(ansi.Color("Updated tran configuration", "green")) 62 | 63 | return nil 64 | }, 65 | } 66 | 67 | var NewConfigGetCmd = &cobra.Command{ 68 | Use: "get", 69 | Short: "Get tran configuration", 70 | Long: "Get tran configuration", 71 | RunE: func(cmd *cobra.Command, args []string) error { 72 | if err != nil { 73 | return err 74 | } 75 | 76 | viper.SetConfigType("yaml") 77 | 78 | viper.ReadConfig(bytes.NewBuffer(tranConfig)) 79 | 80 | fmt.Println(viper.Get("config." + args[0])) 81 | 82 | return nil 83 | }, 84 | } 85 | 86 | var NewConfigListCmd = &cobra.Command{ 87 | Use: "list", 88 | Short: "List tran configuration", 89 | Long: "List tran configuration", 90 | RunE: func(cmd *cobra.Command, args []string) error { 91 | if err != nil { 92 | return err 93 | } 94 | 95 | viper.SetConfigType("yaml") 96 | 97 | viper.ReadConfig(bytes.NewBuffer(tranConfig)) 98 | 99 | // get the config 100 | config := viper.GetStringMap("config") 101 | 102 | for k, v := range config { 103 | fmt.Println(k + " =", v) 104 | } 105 | 106 | return nil 107 | }, 108 | } 109 | -------------------------------------------------------------------------------- /app/gh.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/abdfnx/gh/api" 6 | "github.com/abdfnx/gh/context" 7 | "github.com/abdfnx/gh/core/ghrepo" 8 | "github.com/abdfnx/gh/pkg/cmdutil" 9 | aCmd "github.com/abdfnx/gh/pkg/cmd/auth" 10 | rCmd "github.com/abdfnx/gh/pkg/cmd/gh-repo" 11 | cCmd "github.com/abdfnx/gh/pkg/cmd/gh-config" 12 | ) 13 | 14 | func Auth(f *cmdutil.Factory) *cobra.Command { 15 | cmd := aCmd.NewCmdAuth(f) 16 | return cmd 17 | } 18 | 19 | func GHConfig(f *cmdutil.Factory) *cobra.Command { 20 | cmd := cCmd.NewCmdConfig(f) 21 | return cmd 22 | } 23 | 24 | func Repo(f *cmdutil.Factory) *cobra.Command { 25 | repoResolvingCmdFactory := *f 26 | repoResolvingCmdFactory.BaseRepo = resolvedBaseRepo(f) 27 | 28 | cmd := rCmd.NewCmdRepo(&repoResolvingCmdFactory) 29 | 30 | return cmd 31 | } 32 | 33 | func resolvedBaseRepo(f *cmdutil.Factory) func() (ghrepo.Interface, error) { 34 | return func() (ghrepo.Interface, error) { 35 | httpClient, err := f.HttpClient() 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | apiClient := api.NewClientFromHTTP(httpClient) 41 | 42 | remotes, err := f.Remotes() 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | repoContext, err := context.ResolveRemotesToRepos(remotes, apiClient, "") 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | baseRepo, err := repoContext.BaseRepo(f.IOStreams) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | return baseRepo, nil 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/sync.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | "runtime" 8 | 9 | "github.com/abdfnx/gosh" 10 | "github.com/spf13/cobra" 11 | "github.com/abdfnx/gh/utils" 12 | "github.com/briandowns/spinner" 13 | "github.com/MakeNowJust/heredoc" 14 | "github.com/abdfnx/tran/constants" 15 | git_config "github.com/david-tomson/tran-git" 16 | ) 17 | 18 | var username = git_config.GitConfig() 19 | 20 | var ( 21 | NewCmdStart = &cobra.Command{ 22 | Use: "start", 23 | Aliases: []string{"."}, 24 | Example: "tran sync start", 25 | Short: "Start sync your tran config.", 26 | Run: func(cmd *cobra.Command, args []string) { 27 | if username != ":username" { 28 | exCmd := "echo '# My tran config - " + username + "\n\n## Clone\n\n```\ntran sync clone\n```\n\n**for more about sync command, run `tran sync -h`**' >> $HOME/.tran/README.md" 29 | 30 | gosh.Run(exCmd) 31 | gosh.RunMulti(constants.Start_ml(), constants.Start_w()) 32 | } else { 33 | utils.AuthMessage() 34 | } 35 | }, 36 | } 37 | 38 | NewCmdClone = &cobra.Command{ 39 | Use: "clone", 40 | Aliases: []string{"cn"}, 41 | Short: CloneHelp(), 42 | Run: func(cmd *cobra.Command, args []string) { 43 | if username != ":username" { 44 | gosh.RunMulti(constants.Clone_ml(), constants.Clone_w()) 45 | gosh.RunMulti(constants.Clone_check_ml(), constants.Clone_check_w()) 46 | } else { 47 | utils.AuthMessage() 48 | } 49 | }, 50 | } 51 | 52 | NewCmdPush = &cobra.Command{ 53 | Use: "push", 54 | Aliases: []string{"ph"}, 55 | Short: "Push the new changes in tran config file.", 56 | Run: func(cmd *cobra.Command, args []string) { 57 | if username != ":username" { 58 | gosh.RunMulti(constants.Push_ml(), constants.Push_w()) 59 | } else { 60 | utils.AuthMessage() 61 | } 62 | }, 63 | } 64 | 65 | NewCmdPull = &cobra.Command{ 66 | Use: "pull", 67 | Aliases: []string{"pl"}, 68 | Short: PullHelp(), 69 | Run: func(cmd *cobra.Command, args []string) { 70 | if username != ":username" { 71 | gosh.RunMulti(constants.Pull_ml(), constants.Pull_w()) 72 | } else { 73 | utils.AuthMessage() 74 | } 75 | }, 76 | } 77 | 78 | FetchX = &cobra.Command{ 79 | Use: "fetchx", 80 | Short: "Special command for windows", 81 | Run: func(cmd *cobra.Command, args []string) { 82 | if username != ":username" { 83 | if runtime.GOOS == "windows" { 84 | gosh.PowershellCommand(constants.Clone_w()) 85 | } else { 86 | fmt.Println("This command isn't avaliable for this platform") 87 | } 88 | } else { 89 | utils.AuthMessage() 90 | } 91 | }, 92 | } 93 | ) 94 | 95 | func Sync() *cobra.Command { 96 | cmd := &cobra.Command{ 97 | Use: "sync ", 98 | Short: "Sync your tran config file.", 99 | Long: SyncHelp(), 100 | Example: heredoc.Doc(` 101 | tran sync start 102 | tran sync clone 103 | `), 104 | } 105 | 106 | cmd.AddCommand( 107 | NewCmdStart, 108 | NewCmdClone, 109 | NewCmdPush, 110 | NewCmdPull, 111 | FetchX, 112 | ) 113 | 114 | return cmd 115 | } 116 | 117 | const tranConfigPath string = "/.tran" 118 | 119 | func PullHelp() string { 120 | return git_config.GitConfigWithMsg("Pull the new changes from ", tranConfigPath) 121 | } 122 | 123 | func SyncHelp() string { 124 | return git_config.GitConfigWithMsg("Sync your config file, by create a private repo at ", tranConfigPath) 125 | } 126 | 127 | func CloneHelp() string { 128 | return git_config.GitConfigWithMsg("Clone your .tran from your private repo at https://github.com/", tranConfigPath) 129 | } 130 | 131 | func PushSync() { 132 | const Syncing string = " 📮 Syncing..." 133 | 134 | if runtime.GOOS == "windows" { 135 | err, out, errout := gosh.PowershellOutput( 136 | ` 137 | $directoyPath = "~/.tran/.git" 138 | 139 | if (Test-Path -path $directoyPath) { 140 | Write-Host "Reading from .tran folder..." 141 | } 142 | `) 143 | 144 | fmt.Print(out) 145 | 146 | if err != nil { 147 | log.Printf("error: %v\n", err) 148 | fmt.Print(errout) 149 | } else if out != "" { 150 | s := spinner.New(spinner.CharSets[9], 100*time.Millisecond) 151 | s.Suffix = Syncing 152 | s.Start() 153 | 154 | gosh.PowershellCommand(constants.Push_w()) 155 | 156 | s.Stop() 157 | } 158 | } else { 159 | err, out, errout := gosh.ShellOutput( 160 | ` 161 | if [ -d ~/.tran/.git ]; then 162 | echo "📖 Reading from .tran folder..." 163 | fi 164 | `) 165 | 166 | fmt.Print(out) 167 | 168 | if err != nil { 169 | log.Printf("error: %v\n", err) 170 | fmt.Print(errout) 171 | } else if out != "" { 172 | s := spinner.New(spinner.CharSets[9], 100*time.Millisecond) 173 | s.Suffix = Syncing 174 | s.Start() 175 | 176 | gosh.ShellCommand(constants.Push_ml()) 177 | 178 | s.Stop() 179 | } 180 | } 181 | } 182 | 183 | -------------------------------------------------------------------------------- /cmd/factory/default.go: -------------------------------------------------------------------------------- 1 | package factory 2 | 3 | import ( 4 | "github.com/abdfnx/tran/ios" 5 | ) 6 | 7 | type Factory struct { 8 | IOStreams *ios.IOStreams 9 | } 10 | 11 | func New() *Factory { 12 | f := &Factory{} 13 | 14 | f.IOStreams = ioStreams(f) 15 | 16 | return f 17 | } 18 | 19 | func ioStreams(f *Factory) *ios.IOStreams { 20 | io := ios.System() 21 | 22 | return io 23 | } 24 | -------------------------------------------------------------------------------- /cmd/tran/help-topic.go: -------------------------------------------------------------------------------- 1 | package tran 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | var HelpTopics = map[string]map[string]string{} 8 | 9 | func NewHelpTopic(topic string) *cobra.Command { 10 | cmd := &cobra.Command{ 11 | Use: topic, 12 | Short: HelpTopics[topic]["short"], 13 | Long: HelpTopics[topic]["long"], 14 | Hidden: true, 15 | Annotations: map[string]string{ 16 | "markdown:generate": "true", 17 | "markdown:basename": "tran_help_" + topic, 18 | }, 19 | } 20 | 21 | cmd.SetHelpFunc(helpTopicHelpFunc) 22 | cmd.SetUsageFunc(helpTopicUsageFunc) 23 | 24 | return cmd 25 | } 26 | 27 | func helpTopicHelpFunc(command *cobra.Command, args []string) { 28 | command.Print(command.Long) 29 | } 30 | 31 | func helpTopicUsageFunc(command *cobra.Command) error { 32 | command.Printf("Usage: tran help %s", command.Use) 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /cmd/tran/help.go: -------------------------------------------------------------------------------- 1 | package tran 2 | 3 | import ( 4 | "fmt" 5 | "bytes" 6 | "strings" 7 | 8 | "github.com/spf13/cobra" 9 | "github.com/spf13/pflag" 10 | "github.com/abdfnx/tran/ios" 11 | "github.com/abdfnx/tran/tools" 12 | ) 13 | 14 | func rootUsageFunc(command *cobra.Command) error { 15 | command.Printf("Usage: %s", command.UseLine()) 16 | 17 | subcommands := command.Commands() 18 | 19 | if len(subcommands) > 0 { 20 | command.Print("\n\nCommands:\n") 21 | for _, c := range subcommands { 22 | if c.Hidden { 23 | continue 24 | } 25 | 26 | command.Printf(" %s\n", c.Name()) 27 | } 28 | 29 | return nil 30 | } 31 | 32 | flagUsages := command.LocalFlags().FlagUsages() 33 | 34 | if flagUsages != "" { 35 | command.Println("\n\nFlags:") 36 | command.Print(tools.Indent(dedent(flagUsages), " ")) 37 | } 38 | 39 | return nil 40 | } 41 | 42 | func rootFlagErrorFunc(cmd *cobra.Command, err error) error { 43 | if err == pflag.ErrHelp { 44 | return err 45 | } 46 | 47 | return &tools.FlagError{Err: err} 48 | } 49 | 50 | var hasFailed bool 51 | 52 | func HasFailed() bool { 53 | return hasFailed 54 | } 55 | 56 | func nestedSuggestFunc(command *cobra.Command, arg string) { 57 | command.Printf("unknown command %q for %q\n", arg, command.CommandPath()) 58 | 59 | var candidates []string 60 | if arg == "help" { 61 | candidates = []string{"--help"} 62 | } else { 63 | if command.SuggestionsMinimumDistance <= 0 { 64 | command.SuggestionsMinimumDistance = 2 65 | } 66 | 67 | candidates = command.SuggestionsFor(arg) 68 | } 69 | 70 | if len(candidates) > 0 { 71 | command.Print("\nDid you mean this?\n") 72 | 73 | for _, c := range candidates { 74 | command.Printf("\t%s\n", c) 75 | } 76 | } 77 | 78 | command.Print("\n") 79 | _ = rootUsageFunc(command) 80 | } 81 | 82 | func isRootCmd(command *cobra.Command) bool { 83 | return command != nil && !command.HasParent() 84 | } 85 | 86 | func rootHelpFunc(cs *ios.ColorScheme, command *cobra.Command, args []string) { 87 | if isRootCmd(command.Parent()) && len(args) >= 2 && args[1] != "--help" && args[1] != "-h" { 88 | nestedSuggestFunc(command, args[1]) 89 | hasFailed = true 90 | return 91 | } 92 | 93 | commands := []string{} 94 | 95 | for _, c := range command.Commands() { 96 | if c.Short == "" { 97 | continue 98 | } 99 | if c.Hidden { 100 | continue 101 | } 102 | 103 | s := rpad(c.Name()+":", c.NamePadding()) + c.Short 104 | 105 | commands = append(commands, s) 106 | } 107 | 108 | if len(commands) == 0 { 109 | commands = []string{} 110 | } 111 | 112 | type helpEntry struct { 113 | Title string 114 | Body string 115 | } 116 | 117 | helpEntries := []helpEntry{} 118 | 119 | if command.Long != "" { 120 | helpEntries = append(helpEntries, helpEntry{"", command.Long}) 121 | } else if command.Short != "" { 122 | helpEntries = append(helpEntries, helpEntry{"", command.Short}) 123 | } 124 | 125 | helpEntries = append(helpEntries, helpEntry{"USAGE", command.UseLine()}) 126 | 127 | if len(commands) > 0 { 128 | helpEntries = append(helpEntries, helpEntry{"COMMANDS", strings.Join(commands, "\n")}) 129 | } 130 | 131 | flagUsages := command.LocalFlags().FlagUsages() 132 | 133 | if flagUsages != "" { 134 | helpEntries = append(helpEntries, helpEntry{"FLAGS", dedent(flagUsages)}) 135 | } 136 | 137 | if _, ok := command.Annotations["help:arguments"]; ok { 138 | helpEntries = append(helpEntries, helpEntry{"ARGUMENTS", command.Annotations["help:arguments"]}) 139 | } 140 | 141 | if command.Example != "" { 142 | helpEntries = append(helpEntries, helpEntry{"EXAMPLES", command.Example}) 143 | } 144 | 145 | helpEntries = append(helpEntries, helpEntry{"LEARN MORE", ` 146 | Use 'tran --help' for more information about a command.`}) 147 | if _, ok := command.Annotations["help:tellus"]; ok { 148 | helpEntries = append(helpEntries, helpEntry{"TELL US", command.Annotations["help:tellus"]}) 149 | } 150 | 151 | out := command.OutOrStdout() 152 | for _, e := range helpEntries { 153 | if e.Title != "" { 154 | fmt.Fprintln(out, cs.Bold(e.Title)) 155 | fmt.Fprintln(out, tools.Indent(strings.Trim(e.Body, "\r\n"), " ")) 156 | } else { 157 | fmt.Fprintln(out, e.Body) 158 | } 159 | 160 | fmt.Fprintln(out) 161 | } 162 | } 163 | 164 | func rpad(s string, padding int) string { 165 | template := fmt.Sprintf("%%-%ds ", padding) 166 | return fmt.Sprintf(template, s) 167 | } 168 | 169 | func dedent(s string) string { 170 | lines := strings.Split(s, "\n") 171 | minIndent := -1 172 | 173 | for _, l := range lines { 174 | if len(l) == 0 { 175 | continue 176 | } 177 | 178 | indent := len(l) - len(strings.TrimLeft(l, " ")) 179 | 180 | if minIndent == -1 || indent < minIndent { 181 | minIndent = indent 182 | } 183 | } 184 | 185 | if minIndent <= 0 { 186 | return s 187 | } 188 | 189 | var buf bytes.Buffer 190 | 191 | for _, l := range lines { 192 | fmt.Fprintln(&buf, strings.TrimPrefix(l, strings.Repeat(" ", minIndent))) 193 | } 194 | 195 | return strings.TrimSuffix(buf.String(), "\n") 196 | } 197 | -------------------------------------------------------------------------------- /cmd/tran/root.go: -------------------------------------------------------------------------------- 1 | package tran 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/spf13/cobra" 8 | "github.com/abdfnx/tran/app" 9 | "github.com/MakeNowJust/heredoc" 10 | "github.com/abdfnx/tran/cmd/factory" 11 | "github.com/abdfnx/tran/internal/tui" 12 | "github.com/abdfnx/tran/internal/config" 13 | tea "github.com/charmbracelet/bubbletea" 14 | configCmd "github.com/abdfnx/tran/app/config" 15 | ) 16 | 17 | // Execute start the CLI 18 | func Execute(f *factory.Factory, version string, buildDate string) *cobra.Command { 19 | const desc = `🖥️ Securely transfer and send anything between computers with TUI.` 20 | 21 | // Root command 22 | var rootCmd = &cobra.Command{ 23 | Use: "tran [flags]", 24 | Short: desc, 25 | Long: desc, 26 | SilenceErrors: true, 27 | Example: heredoc.Doc(` 28 | # Open Tran UI 29 | tran 30 | 31 | # Open with specific path 32 | tran --start-dir $PATH 33 | 34 | # Send files to a remote computer 35 | tran send 36 | 37 | # Receive files from a remote computer 38 | tran receive 39 | 40 | # Authenticate 41 | tran auth login 42 | 43 | # Sync your tran config file 44 | tran sync start 45 | `), 46 | Annotations: map[string]string{ 47 | "help:tellus": heredoc.Doc(` 48 | Open an issue at https://github.com/abdfnx/tran/issues 49 | `), 50 | }, 51 | RunE: func(cmd *cobra.Command, args []string) error { 52 | startDir := cmd.Flags().Lookup("start-dir") 53 | 54 | config.LoadConfig(startDir) 55 | cfg := config.GetConfig() 56 | 57 | m := tui.New() 58 | var opts []tea.ProgramOption 59 | 60 | // Always append alt screen program option. 61 | opts = append(opts, tea.WithAltScreen()) 62 | 63 | // If mousewheel is enabled, append it to the program options. 64 | if cfg.Tran.EnableMouseWheel { 65 | opts = append(opts, tea.WithMouseAllMotion()) 66 | } 67 | 68 | // Initialize and start app. 69 | p := tea.NewProgram(m, opts...) 70 | 71 | if err := p.Start(); err != nil { 72 | log.Fatal("Failed to start tran", err) 73 | } 74 | 75 | return nil 76 | }, 77 | } 78 | 79 | versionCmd := &cobra.Command{ 80 | Use: "version", 81 | Aliases: []string{"ver"}, 82 | Short: "Print the version of your tran binary.", 83 | Run: func(cmd *cobra.Command, args []string) { 84 | fmt.Println("tran version " + version + " " + buildDate) 85 | }, 86 | } 87 | 88 | rootCmd.SetOut(f.IOStreams.Out) 89 | rootCmd.SetErr(f.IOStreams.ErrOut) 90 | 91 | cs := f.IOStreams.ColorScheme() 92 | 93 | helpHelper := func(command *cobra.Command, args []string) { 94 | rootHelpFunc(cs, command, args) 95 | } 96 | 97 | rootCmd.PersistentFlags().Bool("help", false, "Help for tran") 98 | rootCmd.PersistentFlags().String("start-dir", "", "Starting directory for Tran") 99 | rootCmd.SetHelpFunc(helpHelper) 100 | rootCmd.SetUsageFunc(rootUsageFunc) 101 | rootCmd.SetFlagErrorFunc(rootFlagErrorFunc) 102 | 103 | // Add sub-commands to root command 104 | rootCmd.AddCommand( 105 | app.NewAuthCmd, 106 | app.NewSendCmd, 107 | app.NewReceiveCmd, 108 | app.NewGHConfigCmd, 109 | app.NewGHRepoCmd, 110 | app.Sync(), 111 | configCmd.NewConfigCmd(), 112 | versionCmd, 113 | ) 114 | 115 | return rootCmd 116 | } 117 | -------------------------------------------------------------------------------- /constants/commands.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | func Fetch_w() string { 4 | return ` 5 | Remove-Item $HOME\.tran -Recurse -Force 6 | tran sync fetchx 7 | Write-Host "Fetched Successfully" 8 | ` 9 | } 10 | 11 | func Fetch_ml() string { 12 | return ` 13 | cd $HOME/.tran 14 | git pull 15 | echo "Fetched Successfully ✅" 16 | ` 17 | } 18 | 19 | func Start_w() string { 20 | return ` 21 | $username = tran auth get-username 22 | cd $HOME\.tran 23 | git init 24 | tran gh-repo create .tran -d "My tran config - $username" --private -y 25 | git add . 26 | git commit -m "new .tran repo" 27 | git branch -M trunk 28 | git remote add origin https://github.com/$username/.tran 29 | git push -u origin trunk 30 | cd $lastDir 31 | ` 32 | } 33 | 34 | func Start_ml() string { 35 | return ` 36 | username=$(tran auth get-username) 37 | cd ~/.tran 38 | git init 39 | tran gh-repo create .tran -d "My tran config - $username" --private -y 40 | git add . 41 | git commit -m "new .tran repo" 42 | git branch -M trunk 43 | git remote add origin https://github.com/$username/.tran 44 | git push -u origin trunk 45 | ` 46 | } 47 | 48 | func Push_w() string { 49 | return ` 50 | $lastDir = pwd 51 | cd $HOME\.tran 52 | if (Test-Path -path .git) { 53 | git add . 54 | git commit -m "new change" 55 | git push 56 | } 57 | 58 | cd $lastDir 59 | ` 60 | } 61 | 62 | func Push_ml() string { 63 | return ` 64 | cd ~/.tran 65 | git add . 66 | git commit -m "new tran config" 67 | git push 68 | ` 69 | } 70 | 71 | func Pull_w() string { 72 | return ` 73 | $lastDir = pwd 74 | cd $HOME\.tran 75 | 76 | git pull 77 | 78 | cd $lastDir 79 | ` 80 | } 81 | 82 | func Pull_ml() string { 83 | return ` 84 | cd ~/.tran 85 | git pull 86 | ` 87 | } 88 | 89 | func Clone_w() string { 90 | return ` 91 | $TRANDIR = $HOME\.tran 92 | 93 | if (Test-Path -path $TRANDIR) { 94 | Remove-Item $TRANDIR -Recurse -Force 95 | } else { 96 | tran gh-repo clone .tran $TRANDIR 97 | } 98 | ` 99 | } 100 | 101 | func Clone_ml() string { 102 | return ` 103 | TRANDIR=~/.tran 104 | 105 | if [ -d $TRANDIR ]; then 106 | rm -rf $TRANDIR 107 | else 108 | tran gh-repo clone .tran $TRANDIR 109 | fi 110 | ` 111 | } 112 | 113 | func Clone_check_w() string { 114 | return ` 115 | if (Test-Path -path $HOME\.tran) { 116 | Write-Host "tran repo cloned successfully" 117 | } 118 | ` 119 | } 120 | 121 | func Clone_check_ml() string { 122 | return `if [ -d $HOME/.tran ]; then echo "tran repo cloned successfully ✅"; fi` 123 | } 124 | -------------------------------------------------------------------------------- /constants/constants.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | "runtime" 7 | "strings" 8 | 9 | "github.com/charmbracelet/lipgloss" 10 | "github.com/charmbracelet/bubbles/progress" 11 | ) 12 | 13 | const DEFAULT_ADDRESS = "167.71.65.96" 14 | const DEFAULT_PORT = 80 15 | 16 | const MAX_CHUNK_BYTES = 1e6 17 | const MAX_SEND_CHUNKS = 2e8 18 | 19 | const RECEIVER_CONNECT_TIMEOUT time.Duration = 5 * time.Minute 20 | 21 | const SEND_TEMP_FILE_NAME_PREFIX = "tran-send-tmp" 22 | const RECEIVE_TEMP_FILE_NAME_PREFIX = "tran-receive-tmp" 23 | 24 | const ( 25 | PrimaryBoxActive = iota 26 | SecondaryBoxActive 27 | ThirdBoxActive 28 | ) 29 | 30 | const ( 31 | StatusBarHeight = 1 32 | BoxPadding = 1 33 | EllipsisStyle = "..." 34 | FileSizeLoadingStyle = "---" 35 | ) 36 | 37 | var BoldTextStyle = lipgloss.NewStyle().Bold(true) 38 | 39 | var Colors = map[string]lipgloss.Color{ 40 | "black": "#000000", 41 | } 42 | 43 | const ( 44 | PADDING = 2 45 | MAX_WIDTH = 80 46 | PRIMARY_COLOR = "#1E90FF" 47 | SECONDARY_COLOR = "#1E6AFF" 48 | DARK_GRAY_COLOR = "#3c3836" 49 | START_PERIOD = 1 * time.Millisecond 50 | SHUTDOWN_PERIOD = 1000 * time.Millisecond 51 | ) 52 | 53 | var QuitKeys = []string{"q", "esc"} 54 | var PadText = strings.Repeat(" ", PADDING) 55 | var QuitCommandsHelpText = HelpStyle(fmt.Sprintf("(press one of [%s] keys to quit from tran)", (strings.Join(QuitKeys, ", ")))) 56 | var ProgressBar = progress.NewModel(progress.WithGradient(SECONDARY_COLOR, PRIMARY_COLOR)) 57 | 58 | var baseStyle = lipgloss.NewStyle() 59 | var InfoStyle = baseStyle.Copy().Foreground(lipgloss.Color(PRIMARY_COLOR)).Render 60 | var HelpStyle = baseStyle.Copy().Foreground(lipgloss.Color(DARK_GRAY_COLOR)).Render 61 | var ItalicText = baseStyle.Copy().Italic(true).Render 62 | var BoldText = baseStyle.Copy().Bold(true).Render 63 | 64 | func CtrlKey() string { 65 | // if os is macos, then return "⌘" 66 | if runtime.GOOS == "darwin" { 67 | return "⌘" 68 | } else { 69 | return "ctrl" 70 | } 71 | } 72 | 73 | func AltKey() string { 74 | // if os is macos, then return "⌥" 75 | if runtime.GOOS == "darwin" { 76 | return "⌥" 77 | } else { 78 | return "alt" 79 | } 80 | } 81 | 82 | var HelpContent = `# Help Guide` + "\n" + 83 | "* `tab`: Switch between boxes\n" + 84 | "* `up`: Move up\n" + 85 | "* `down`: Move down\n" + 86 | "* `left`: Go back a directory\n" + 87 | "* `right`: Read file or enter directory\n" + 88 | "* `V`: View directory\n" + 89 | "* `T`: Go to top\n" + 90 | "* `G`: Go to bottom\n" + 91 | "* `~`: Go to your home directory\n" + 92 | "* `/`: Go to root directory\n" + 93 | "* `.`: Toggle hidden files and directories\n" + 94 | "* `D`: Only show directories\n" + 95 | "* `F`: Only show files\n" + 96 | "* `E`: Edit file\n" + 97 | "* `" + CtrlKey() + "+s`: Send files/directories to remote\n" + 98 | "* `" + CtrlKey() + "+r`: Receive files/directories from remote\n" + 99 | "* `" + CtrlKey() + "+f`: Find files and directories by name\n" + 100 | "* `q`/`" + CtrlKey() + "+q`: Quit" 101 | 102 | var InfoContent = `# Info` + "\n" + 103 | "* Address: **" + DEFAULT_ADDRESS + "**\n" + 104 | "* Port: **" + fmt.Sprintf("%d", DEFAULT_PORT) + "**\n" + 105 | "* OS: **" + runtime.GOOS + "**\n" + 106 | "* Arch: **" + runtime.GOARCH + "**\n" + 107 | "* Author: " + "[**@abdfnx**](https://github.com/abdfnx)" 108 | -------------------------------------------------------------------------------- /core/crypt/crypt.go: -------------------------------------------------------------------------------- 1 | package crypt 2 | 3 | import ( 4 | "fmt" 5 | "crypto/aes" 6 | "crypto/rand" 7 | "crypto/cipher" 8 | "crypto/sha256" 9 | 10 | "golang.org/x/crypto/pbkdf2" 11 | ) 12 | 13 | type Crypt struct { 14 | Key []byte 15 | Salt []byte 16 | } 17 | 18 | // New returns a new Crypt object, with a sha256 cryptographic key and corresponding salt. 19 | // Parameters: 20 | // sessionkey A sessionkey (preferably generated with PAKE2). 21 | // salt Salt used in generating the key. If not supplied a random salt will be generated. 22 | func New(sessionkey []byte, salt ...[]byte) (*Crypt, error) { 23 | var s []byte 24 | 25 | if len(salt) < 1 { 26 | s = make([]byte, 8) 27 | 28 | if _, err := rand.Read(s); err != nil { 29 | return nil, fmt.Errorf("unable to generate random salt: %v", err) 30 | } 31 | } else { 32 | s = salt[0] 33 | } 34 | 35 | key := pbkdf2.Key(sessionkey, s, 100, 32, sha256.New) 36 | 37 | crypt := &Crypt{ 38 | Key: key, 39 | Salt: s, 40 | } 41 | 42 | return crypt, nil 43 | } 44 | 45 | // Encrypt encrypts the provided message using shared key and a random nonce that is appended to the message. 46 | func (s *Crypt) Encrypt(unencrypted []byte) (encrypted []byte, err error) { 47 | block, err := aes.NewCipher(s.Key) 48 | 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | nonce := make([]byte, 12) 54 | 55 | if _, err := rand.Read(nonce); err != nil { 56 | return nil, fmt.Errorf("unable to generate random nonce: %v", err) 57 | } 58 | 59 | aescgm, err := cipher.NewGCM(block) 60 | 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | encrypted = aescgm.Seal(nil, nonce, unencrypted, nil) 66 | encrypted = append(nonce, encrypted...) 67 | 68 | return encrypted, nil 69 | } 70 | 71 | // Decrypt decrypts the provided message with the the shared key. 72 | func (s *Crypt) Decrypt(encrypted []byte) (decrypted []byte, err error) { 73 | block, err := aes.NewCipher(s.Key) 74 | 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | aescgm, err := cipher.NewGCM(block) 80 | 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | decrypted, err = aescgm.Open(nil, encrypted[:12], encrypted[12:], nil) 86 | 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | return decrypted, nil 92 | } 93 | -------------------------------------------------------------------------------- /core/receiver/receive.go: -------------------------------------------------------------------------------- 1 | package receiver 2 | 3 | import ( 4 | "io" 5 | "encoding/json" 6 | 7 | "github.com/gorilla/websocket" 8 | "github.com/abdfnx/tran/tools" 9 | "github.com/abdfnx/tran/models/protocol" 10 | ) 11 | 12 | func (r *Receiver) Receive(wsConn *websocket.Conn, buffer io.Writer) error { 13 | // request payload 14 | tools.WriteEncryptedMessage(wsConn, protocol.TransferMessage{Type: protocol.ReceiverRequestPayload}, r.crypt) 15 | 16 | var writtenBytes int64 17 | for { 18 | _, encBytes, err := wsConn.ReadMessage() 19 | if err != nil { 20 | return err 21 | } 22 | 23 | decBytes, err := r.crypt.Decrypt(encBytes) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | transferMsg := protocol.TransferMessage{} 29 | err = json.Unmarshal(decBytes, &transferMsg) 30 | if err != nil { 31 | buffer.Write(decBytes) 32 | writtenBytes += int64(len(decBytes)) 33 | r.updateUI(float32(writtenBytes) / float32(r.payloadSize)) 34 | } else { 35 | if transferMsg.Type != protocol.SenderPayloadSent { 36 | return protocol.NewWrongMessageTypeError([]protocol.TransferMessageType{protocol.SenderPayloadSent}, transferMsg.Type) 37 | } 38 | break 39 | } 40 | } 41 | 42 | // ACK received payload 43 | tools.WriteEncryptedMessage(wsConn, protocol.TransferMessage{Type: protocol.ReceiverPayloadAck}, r.crypt) 44 | 45 | transferMsg, err := tools.ReadEncryptedMessage(wsConn, r.crypt) 46 | if err != nil { 47 | return err 48 | } 49 | if transferMsg.Type != protocol.SenderClosing { 50 | return protocol.NewWrongMessageTypeError([]protocol.TransferMessageType{protocol.SenderClosing}, transferMsg.Type) 51 | } 52 | 53 | // ACK SenderClosing with ReceiverClosing 54 | tools.WriteEncryptedMessage(wsConn, protocol.TransferMessage{Type: protocol.ReceiverClosingAck}, r.crypt) 55 | 56 | return err 57 | } 58 | -------------------------------------------------------------------------------- /core/receiver/receiver.go: -------------------------------------------------------------------------------- 1 | package receiver 2 | 3 | import ( 4 | "github.com/abdfnx/tran/models" 5 | "github.com/abdfnx/tran/core/crypt" 6 | ) 7 | 8 | type Receiver struct { 9 | crypt *crypt.Crypt 10 | payloadSize int64 11 | tranxAddress string 12 | tranxPort int 13 | ui chan<- UIUpdate 14 | usedRelay bool 15 | } 16 | 17 | func NewReceiver(programOptions models.TranOptions) *Receiver { 18 | return &Receiver{ 19 | tranxAddress: programOptions.TranxAddress, 20 | tranxPort: programOptions.TranxPort, 21 | } 22 | } 23 | 24 | func WithUI(r *Receiver, ui chan<- UIUpdate) *Receiver { 25 | r.ui = ui 26 | return r 27 | } 28 | 29 | func (r *Receiver) UsedRelay() bool { 30 | return r.usedRelay 31 | } 32 | 33 | func (r *Receiver) PayloadSize() int64 { 34 | return r.payloadSize 35 | } 36 | 37 | func (r *Receiver) TranxAddress() string { 38 | return r.tranxAddress 39 | } 40 | 41 | func (r *Receiver) TranxPort() int { 42 | return r.tranxPort 43 | } 44 | 45 | func (r *Receiver) updateUI(progress float32) { 46 | if r.ui == nil { 47 | return 48 | } 49 | 50 | r.ui <- UIUpdate{Progress: progress} 51 | } 52 | -------------------------------------------------------------------------------- /core/receiver/state.go: -------------------------------------------------------------------------------- 1 | package receiver 2 | 3 | type UIUpdate struct { 4 | Progress float32 5 | } 6 | -------------------------------------------------------------------------------- /core/receiver/tranx-client.go: -------------------------------------------------------------------------------- 1 | package receiver 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "time" 7 | "context" 8 | 9 | "github.com/schollz/pake/v3" 10 | "github.com/abdfnx/tran/tools" 11 | "github.com/gorilla/websocket" 12 | "github.com/abdfnx/tran/models" 13 | "github.com/abdfnx/tran/core/crypt" 14 | "github.com/abdfnx/tran/models/protocol" 15 | ) 16 | 17 | func (r *Receiver) ConnectToTranx(tranxAddress string, tranxPort int, password models.Password) (*websocket.Conn, error) { 18 | // establish websocket connection to tranx server 19 | tranxConn, _, err := websocket.DefaultDialer.Dial(fmt.Sprintf("ws://%s:%d/establish-receiver", tranxAddress, tranxPort), nil) 20 | 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | err = r.establishSecureConnection(tranxConn, password) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | senderIP, senderPort, err := r.doTransferHandshake(tranxConn) 31 | 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | directConn, err := r.probeSender(senderIP, senderPort) 37 | 38 | if err == nil { 39 | // notify sender through tranx that we will be using direct communication 40 | tools.WriteEncryptedMessage(tranxConn, protocol.TransferMessage{Type: protocol.ReceiverDirectCommunication}, r.crypt) 41 | // tell tranx to close the connection 42 | tranxConn.WriteJSON(protocol.TranxMessage{Type: protocol.ReceiverToTranxClose}) 43 | 44 | return directConn, nil 45 | } 46 | 47 | r.usedRelay = true 48 | tools.WriteEncryptedMessage(tranxConn, protocol.TransferMessage{Type: protocol.ReceiverRelayCommunication}, r.crypt) 49 | 50 | transferMsg, err := tools.ReadEncryptedMessage(tranxConn, r.crypt) 51 | 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | if transferMsg.Type != protocol.SenderRelayAck { 57 | return nil, err 58 | } 59 | 60 | return tranxConn, nil 61 | } 62 | 63 | func (r *Receiver) probeSender(senderIP net.IP, senderPort int) (*websocket.Conn, error) { 64 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 65 | defer cancel() 66 | 67 | d := 250 * time.Millisecond 68 | 69 | for { 70 | select { 71 | case <-ctx.Done(): 72 | return nil, fmt.Errorf("could not establish a connection to the sender server") 73 | 74 | default: 75 | dialer := websocket.Dialer{HandshakeTimeout: d} 76 | wsConn, _, err := dialer.Dial(fmt.Sprintf("ws://%s:%d/tran", senderIP.String(), senderPort), nil) 77 | 78 | if err != nil { 79 | time.Sleep(d) 80 | d = d * 2 81 | continue 82 | } 83 | 84 | return wsConn, nil 85 | } 86 | } 87 | } 88 | 89 | func (r *Receiver) doTransferHandshake(wsConn *websocket.Conn) (net.IP, int, error) { 90 | tcpAddr, _ := wsConn.LocalAddr().(*net.TCPAddr) 91 | 92 | msg := protocol.TransferMessage{ 93 | Type: protocol.ReceiverHandshake, 94 | Payload: protocol.ReceiverHandshakePayload{ 95 | IP: tcpAddr.IP, 96 | }, 97 | } 98 | 99 | err := tools.WriteEncryptedMessage(wsConn, msg, r.crypt) 100 | if err != nil { 101 | return nil, 0, err 102 | } 103 | 104 | msg, err = tools.ReadEncryptedMessage(wsConn, r.crypt) 105 | if err != nil { 106 | return nil, 0, err 107 | } 108 | 109 | if msg.Type != protocol.SenderHandshake { 110 | return nil, 0, protocol.NewWrongMessageTypeError([]protocol.TransferMessageType{protocol.SenderHandshake}, msg.Type) 111 | } 112 | 113 | handshakePayload := protocol.SenderHandshakePayload{} 114 | err = tools.DecodePayload(msg.Payload, &handshakePayload) 115 | 116 | if err != nil { 117 | return nil, 0, err 118 | } 119 | 120 | r.payloadSize = handshakePayload.PayloadSize 121 | 122 | return handshakePayload.IP, handshakePayload.Port, nil 123 | } 124 | 125 | func (r *Receiver) establishSecureConnection(wsConn *websocket.Conn, password models.Password) error { 126 | // init curve in background 127 | pakeCh := make(chan *pake.Pake) 128 | pakeErr := make(chan error) 129 | 130 | go func() { 131 | var err error 132 | p, err := pake.InitCurve([]byte(password), 1, "p256") 133 | pakeErr <- err 134 | pakeCh <- p 135 | }() 136 | 137 | wsConn.WriteJSON(protocol.TranxMessage{ 138 | Type: protocol.ReceiverToTranxEstablish, 139 | Payload: protocol.PasswordPayload{ 140 | Password: tools.HashPassword(password), 141 | }, 142 | }) 143 | 144 | msg, err := tools.ReadTranxMessage(wsConn, protocol.TranxToReceiverPAKE) 145 | if err != nil { 146 | return err 147 | } 148 | 149 | pakePayload := protocol.PakePayload{} 150 | err = tools.DecodePayload(msg.Payload, &pakePayload) 151 | 152 | if err != nil { 153 | return err 154 | } 155 | 156 | // check if we had an issue with the PAKE2 initialization error 157 | if err = <-pakeErr; err != nil { 158 | return err 159 | } 160 | 161 | p := <-pakeCh 162 | 163 | err = p.Update(pakePayload.Bytes) 164 | if err != nil { 165 | return err 166 | } 167 | 168 | wsConn.WriteJSON(protocol.TranxMessage{ 169 | Type: protocol.ReceiverToTranxPAKE, 170 | Payload: protocol.PakePayload{ 171 | Bytes: p.Bytes(), 172 | }, 173 | }) 174 | 175 | msg, err = tools.ReadTranxMessage(wsConn, protocol.TranxToReceiverSalt) 176 | 177 | if err != nil { 178 | return err 179 | } 180 | 181 | saltPayload := protocol.SaltPayload{} 182 | err = tools.DecodePayload(msg.Payload, &saltPayload) 183 | 184 | if err != nil { 185 | return err 186 | } 187 | 188 | sessionKey, err := p.SessionKey() 189 | if err != nil { 190 | return err 191 | } 192 | 193 | r.crypt, err = crypt.New(sessionKey, saltPayload.Salt) 194 | if err != nil { 195 | return err 196 | } 197 | 198 | return nil 199 | } 200 | -------------------------------------------------------------------------------- /core/sender/handlers.go: -------------------------------------------------------------------------------- 1 | package sender 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net" 7 | "syscall" 8 | "net/http" 9 | ) 10 | 11 | // handleTransfer creates a HandlerFunc to handle serving the transfer of files over a websocket connection 12 | func (s *Sender) handleTransfer() http.HandlerFunc { 13 | return func(w http.ResponseWriter, r *http.Request) { 14 | if s.receiverIP.Equal(net.ParseIP(r.RemoteAddr)) { 15 | w.WriteHeader(http.StatusForbidden) 16 | fmt.Fprintf(w, "No Tran for You!") 17 | log.Printf("Unauthorized Tran attempt from alien species with IP: %s\n", r.RemoteAddr) 18 | 19 | return 20 | } 21 | 22 | wsConn, err := s.senderServer.upgrader.Upgrade(w, r, nil) 23 | if err != nil { 24 | log.Printf("Unable to initialize Tran due to technical error: %s\n", err) 25 | s.closeServer <- syscall.SIGTERM 26 | 27 | return 28 | } 29 | 30 | // Start transfer sequence. 31 | s.Transfer(wsConn) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /core/sender/sender.go: -------------------------------------------------------------------------------- 1 | package sender 2 | 3 | import ( 4 | "os" 5 | "io" 6 | "fmt" 7 | "net" 8 | "time" 9 | "syscall" 10 | "net/http" 11 | "os/signal" 12 | 13 | "github.com/gorilla/websocket" 14 | "github.com/abdfnx/tran/models" 15 | "github.com/abdfnx/tran/core/crypt" 16 | ) 17 | 18 | // Sender represents the sender client, handles tranx communication and file transfer. 19 | type Sender struct { 20 | payload io.Reader 21 | payloadSize int64 22 | senderServer *Server 23 | closeServer chan os.Signal 24 | receiverIP net.IP 25 | tranxAddress string 26 | tranxPort int 27 | ui chan<- UIUpdate 28 | crypt *crypt.Crypt 29 | state TransferState 30 | } 31 | 32 | // NewSender returns a bare bones Sender. 33 | func NewSender(programOptions models.TranOptions) *Sender { 34 | closeServerCh := make(chan os.Signal, 1) 35 | signal.Notify(closeServerCh, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) 36 | 37 | return &Sender{ 38 | closeServer: closeServerCh, 39 | tranxAddress: programOptions.TranxAddress, 40 | tranxPort: programOptions.TranxPort, 41 | state: Initial, 42 | } 43 | } 44 | 45 | // WithPayload specifies the payload that will be transfered. 46 | func WithPayload(s *Sender, payload io.Reader, payloadSize int64) *Sender { 47 | s.payload = payload 48 | s.payloadSize = payloadSize 49 | 50 | return s 51 | } 52 | 53 | // WithServer specifies the option to run the sender by hosting a server which the receiver establishes a connection to. 54 | func WithServer(s *Sender, options ServerOptions) *Sender { 55 | s.receiverIP = options.receiverIP 56 | router := &http.ServeMux{} 57 | s.senderServer = &Server{ 58 | router: router, 59 | server: &http.Server{ 60 | Addr: fmt.Sprintf(":%d", options.port), 61 | ReadTimeout: 30 * time.Second, 62 | WriteTimeout: 30 * time.Second, 63 | Handler: router, 64 | }, 65 | upgrader: websocket.Upgrader{}, 66 | } 67 | 68 | // setup routes 69 | router.HandleFunc("/tran", s.handleTransfer()) 70 | return s 71 | } 72 | 73 | // WithUI specifies the option to run the sender with an UI channel that reports the state of the transfer. 74 | func WithUI(s *Sender, ui chan<- UIUpdate) *Sender { 75 | s.ui = ui 76 | 77 | return s 78 | } 79 | 80 | func (s *Sender) TranxAddress() string { 81 | return s.tranxAddress 82 | } 83 | 84 | func (s *Sender) TranxPort() int { 85 | return s.tranxPort 86 | } 87 | 88 | // updateUI is a helper function that checks if we have a UI channel and reports the state. 89 | func (s *Sender) updateUI(progress ...float32) { 90 | if s.ui == nil { 91 | return 92 | } 93 | 94 | var p float32 95 | 96 | if len(progress) > 0 { 97 | p = progress[0] 98 | } 99 | 100 | s.ui <- UIUpdate{State: s.state, Progress: p} 101 | } 102 | -------------------------------------------------------------------------------- /core/sender/server.go: -------------------------------------------------------------------------------- 1 | package sender 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net" 7 | "time" 8 | "syscall" 9 | "context" 10 | "net/http" 11 | 12 | "github.com/gorilla/websocket" 13 | ) 14 | 15 | // Server specifies the webserver that will be used for direct file transfer. 16 | type Server struct { 17 | server *http.Server 18 | router *http.ServeMux 19 | upgrader websocket.Upgrader 20 | } 21 | 22 | // Specifies the necessary options for initializing the webserver. 23 | type ServerOptions struct { 24 | port int 25 | receiverIP net.IP 26 | } 27 | 28 | // Start starts the sender.Server webserver and setups graceful shutdown 29 | func (s *Sender) StartServer() error { 30 | if s.senderServer == nil { 31 | return fmt.Errorf("start called with uninitialized senderServer") 32 | } 33 | 34 | // context used for graceful shutdown 35 | ctx, cancel := context.WithCancel(context.Background()) 36 | 37 | go func() { 38 | <-s.closeServer 39 | cancel() 40 | }() 41 | 42 | // serve the webserver, and report errors 43 | if err := serve(s, ctx); err != nil { 44 | return err 45 | } 46 | 47 | return nil 48 | } 49 | 50 | func (s *Sender) CloseServer() { 51 | s.closeServer <- syscall.SIGTERM 52 | } 53 | 54 | // serve is helper function that serves the webserver while providing graceful shutdown. 55 | func serve(s *Sender, ctx context.Context) (err error) { 56 | if s.senderServer == nil { 57 | return fmt.Errorf("serve called with uninitialized senderServer") 58 | } 59 | 60 | go func() { 61 | if err = s.senderServer.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { 62 | log.Fatalf("Tran sender-server crashed due to an error: %s\n", err) 63 | } 64 | }() 65 | 66 | <-ctx.Done() // wait for the shutdown sequence to start. 67 | 68 | ctxShutdown, cancel := context.WithTimeout(context.Background(), 5*time.Second) 69 | defer func() { 70 | cancel() 71 | }() 72 | 73 | // shutdown and report errors 74 | if err = s.senderServer.server.Shutdown(ctxShutdown); err != nil { 75 | log.Fatalf("Tran shutdown sequence failed to due error: %s\n", err) 76 | } 77 | 78 | // strip error in this case, as we deal with this gracefully 79 | if err == http.ErrServerClosed { 80 | err = nil 81 | } 82 | 83 | return err 84 | } 85 | -------------------------------------------------------------------------------- /core/sender/state.go: -------------------------------------------------------------------------------- 1 | package sender 2 | 3 | import "fmt" 4 | 5 | type TransferState int 6 | 7 | const ( 8 | Initial TransferState = iota 9 | WaitForFileRequest 10 | SendingData 11 | WaitForFileAck 12 | WaitForCloseMessage 13 | WaitForCloseAck 14 | ) 15 | 16 | // UIUpdate is a struct that is continously communicated to the UI (if sender has attached a UI) 17 | type UIUpdate struct { 18 | State TransferState 19 | Progress float32 20 | } 21 | 22 | // WrongStateError is a custom error for the Transfer sequence 23 | type WrongStateError struct { 24 | expected TransferState 25 | got TransferState 26 | } 27 | 28 | // WrongStateError constructor 29 | func NewWrongStateError(expected, got TransferState) *WrongStateError { 30 | return &WrongStateError{ 31 | expected: expected, 32 | got: got, 33 | } 34 | } 35 | 36 | func (e *WrongStateError) Error() string { 37 | return fmt.Sprintf("wrong message type, expected type: %d(%s), got: %d(%s)", e.expected, e.expected.Name(), e.got, e.got.Name()) 38 | } 39 | 40 | // Name returns the associated to the state enum. 41 | func (s TransferState) Name() string { 42 | switch s { 43 | case Initial: 44 | return "Initial" 45 | 46 | case WaitForFileRequest: 47 | return "WaitForFileRequest" 48 | 49 | case SendingData: 50 | return "SendingData" 51 | 52 | case WaitForFileAck: 53 | return "WaitForFileAck" 54 | 55 | case WaitForCloseMessage: 56 | return "WaitForCloseMessage" 57 | 58 | case WaitForCloseAck: 59 | return "WaitForCloseAck" 60 | 61 | default: 62 | return "" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /core/sender/transfer.go: -------------------------------------------------------------------------------- 1 | package sender 2 | 3 | import ( 4 | "io" 5 | "fmt" 6 | "log" 7 | "bufio" 8 | "syscall" 9 | 10 | "github.com/gorilla/websocket" 11 | "github.com/abdfnx/tran/tools" 12 | "github.com/abdfnx/tran/constants" 13 | "github.com/abdfnx/tran/models/protocol" 14 | ) 15 | 16 | // Transfer is the file transfer sequence, can be via relay or tranx. 17 | func (s *Sender) Transfer(wsConn *websocket.Conn) error { 18 | s.state = WaitForFileRequest 19 | 20 | for { 21 | // Read incoming message. 22 | receivedMsg, err := tools.ReadEncryptedMessage(wsConn, s.crypt) 23 | if err != nil { 24 | wsConn.Close() 25 | s.closeServer <- syscall.SIGTERM 26 | return fmt.Errorf("shutting down tran due to websocket error: %s", err) 27 | } 28 | 29 | // main switch for action based on incoming message. 30 | // The states flows from top down. States checks are performend at each step. 31 | switch receivedMsg.Type { 32 | case protocol.ReceiverRequestPayload: 33 | if s.state != WaitForFileRequest { 34 | err = tools.WriteEncryptedMessage(wsConn, protocol.TransferMessage{ 35 | Type: protocol.TransferError, 36 | Payload: fmt.Sprintf("Tran unsynchronized, expected state: %s, actual: %s", WaitForFileRequest.Name(), s.state.Name()), 37 | }, s.crypt) 38 | 39 | if err != nil { 40 | return err 41 | } 42 | 43 | wsConn.Close() 44 | s.closeServer <- syscall.SIGTERM 45 | 46 | return NewWrongStateError(WaitForFileRequest, s.state) 47 | } 48 | 49 | err = s.streamPayload(wsConn) 50 | if err != nil { 51 | log.Println("error in payload streaming:", err) 52 | 53 | return err 54 | } 55 | 56 | err = tools.WriteEncryptedMessage(wsConn, protocol.TransferMessage{ 57 | Type: protocol.SenderPayloadSent, 58 | Payload: "Tran transfer completed", 59 | }, s.crypt) 60 | 61 | if err != nil { 62 | return err 63 | } 64 | 65 | s.state = WaitForFileAck 66 | s.updateUI() 67 | 68 | case protocol.ReceiverPayloadAck: 69 | if s.state != WaitForFileAck { 70 | err = tools.WriteEncryptedMessage(wsConn, protocol.TransferMessage{ 71 | Type: protocol.TransferError, 72 | Payload: fmt.Sprintf("Tran unsynchronized, expected state: %s, actual: %s", WaitForFileAck.Name(), s.state.Name()), 73 | }, s.crypt) 74 | 75 | if err != nil { 76 | return err 77 | } 78 | 79 | wsConn.Close() 80 | s.closeServer <- syscall.SIGTERM 81 | 82 | return NewWrongStateError(WaitForFileAck, s.state) 83 | } 84 | 85 | s.state = WaitForCloseMessage 86 | s.updateUI() 87 | 88 | err = tools.WriteEncryptedMessage(wsConn, protocol.TransferMessage{ 89 | Type: protocol.SenderClosing, 90 | Payload: "Closing down Tran as requested", 91 | }, s.crypt) 92 | 93 | if err != nil { 94 | return err 95 | } 96 | 97 | s.state = WaitForCloseAck 98 | s.updateUI() 99 | 100 | case protocol.ReceiverClosingAck: 101 | wsConn.Close() 102 | s.closeServer <- syscall.SIGTERM 103 | 104 | if s.state != WaitForCloseAck { 105 | return NewWrongStateError(WaitForCloseAck, s.state) 106 | } 107 | 108 | return nil 109 | 110 | case protocol.TransferError: 111 | s.updateUI() 112 | log.Println("Shutting down Tran due to a transfer error") 113 | wsConn.Close() 114 | s.closeServer <- syscall.SIGTERM 115 | 116 | return fmt.Errorf("TransferError during file transfer") 117 | } 118 | } 119 | } 120 | 121 | // streamPayload streams the payload over the provided websocket connection while reporting the progress. 122 | func (s *Sender) streamPayload(wsConn *websocket.Conn) error { 123 | bufReader := bufio.NewReader(s.payload) 124 | chunkSize := ChunkSize(s.payloadSize) 125 | buffer := make([]byte, chunkSize) 126 | 127 | var bytesSent int 128 | 129 | for { 130 | n, err := bufReader.Read(buffer) 131 | bytesSent += n 132 | enc, encErr := s.crypt.Encrypt(buffer[:n]) 133 | 134 | if encErr != nil { 135 | return encErr 136 | } 137 | 138 | wsConn.WriteMessage(websocket.BinaryMessage, enc) 139 | progress := float32(bytesSent) / float32(s.payloadSize) 140 | s.updateUI(progress) 141 | 142 | if err == io.EOF { 143 | break 144 | } 145 | } 146 | 147 | return nil 148 | } 149 | 150 | // ChunkSize returns an appropriate chunk size for the payload size 151 | func ChunkSize(payloadSize int64) int64 { 152 | // clamp amount of chunks to be at most MAX_SEND_CHUNKS if it exceeds 153 | if payloadSize / constants.MAX_CHUNK_BYTES > constants.MAX_SEND_CHUNKS { 154 | return int64(payloadSize) / constants.MAX_SEND_CHUNKS 155 | } 156 | // if not exceeding MAX_SEND_CHUNKS, divide up no. of chunks to MAX_CHUNK_BYTES-sized chunks 157 | chunkSize := int64(payloadSize) / constants.MAX_CHUNK_BYTES 158 | // clamp amount of chunks to be at least MAX_CHUNK_BYTES 159 | if chunkSize <= constants.MAX_CHUNK_BYTES { 160 | return constants.MAX_CHUNK_BYTES 161 | } 162 | 163 | return chunkSize 164 | } 165 | -------------------------------------------------------------------------------- /core/sender/tranx-client.go: -------------------------------------------------------------------------------- 1 | package sender 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/schollz/pake/v3" 8 | "github.com/gorilla/websocket" 9 | "github.com/abdfnx/tran/tools" 10 | "github.com/abdfnx/tran/models" 11 | "github.com/abdfnx/tran/core/crypt" 12 | "github.com/abdfnx/tran/models/protocol" 13 | ) 14 | 15 | // ConnectToTranx, establishes the connection with the tranx server. 16 | // Paramaters: 17 | // tranxAddress - IP or hostname of the tranx server 18 | // tranxPort - port of the tranx server 19 | // startServerCh - channel to communicate to the caller when to start the server, and with which options. 20 | // passwordCh - channel to communicate the password to the caller. 21 | // startServerCh - channel to communicate to the caller when to start the server, and with which options. 22 | // payloadReady - channel over which the caller can communicate when the payload is ready. 23 | // relayCh - channel to commuincate if we are using relay (tranx) for transfer. 24 | func (s *Sender) ConnectToTranx( 25 | tranxAddress string, 26 | tranxPort int, 27 | passwordCh chan<- models.Password, 28 | startServerCh chan<- ServerOptions, 29 | payloadReady <-chan bool, 30 | relayCh chan<- *websocket.Conn, 31 | ) error { 32 | // establish websocket connection to tranx server 33 | wsConn, _, err := websocket.DefaultDialer.Dial(fmt.Sprintf("ws://%s:%d/establish-sender", tranxAddress, tranxPort), nil) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | // bind connection 39 | tranxMsg, err := tools.ReadTranxMessage(wsConn, protocol.TranxToSenderBind) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | bindPayload := protocol.TranxToSenderBindPayload{} 45 | err = tools.DecodePayload(tranxMsg.Payload, &bindPayload) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | // establish sender 51 | password := tools.GeneratePassword(bindPayload.ID) 52 | hashed := tools.HashPassword(password) 53 | 54 | wsConn.WriteJSON(protocol.TranxMessage{ 55 | Type: protocol.SenderToTranxEstablish, 56 | Payload: protocol.PasswordPayload{ 57 | Password: hashed, 58 | }, 59 | }) 60 | 61 | // send the generated password to the UI so it can be displayed 62 | passwordCh <- password 63 | 64 | // setup the encryption 65 | err = s.establishSecureConnection(wsConn, password) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | // do the transfer handshake over the tranx 71 | err = s.doHandshake(wsConn, payloadReady, startServerCh) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | transferMsg, err := tools.ReadEncryptedMessage(wsConn, s.crypt) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | switch transferMsg.Type { 82 | // we will do direct communication with the receiver 83 | case protocol.ReceiverDirectCommunication: 84 | close(relayCh) 85 | tools.WriteEncryptedMessage(wsConn, protocol.TransferMessage{Type: protocol.SenderDirectAck}, s.crypt) 86 | 87 | return nil 88 | 89 | // we will do relay communication with receiver using the same websocket connection as with tranx 90 | case protocol.ReceiverRelayCommunication: 91 | tools.WriteEncryptedMessage(wsConn, protocol.TransferMessage{Type: protocol.SenderRelayAck}, s.crypt) 92 | relayCh <- wsConn 93 | 94 | return nil 95 | 96 | default: 97 | return protocol.NewWrongMessageTypeError( 98 | []protocol.TransferMessageType{protocol.ReceiverDirectCommunication, protocol.ReceiverRelayCommunication}, 99 | transferMsg.Type) 100 | } 101 | } 102 | 103 | // establishSecureConnection setups the PAKE2 key exchange and the crypt struct in the sender. 104 | func (s *Sender) establishSecureConnection(wsConn *websocket.Conn, password models.Password) error { 105 | // init PAKE2 (NOTE: This takes a couple of seconds, here it is fine as we have to wait for the receiver) 106 | pake, err := pake.InitCurve([]byte(password), 0, "p256") 107 | 108 | if err != nil { 109 | return err 110 | } 111 | 112 | // Wait for receiver to be ready to exchange crypto information. 113 | msg, err := tools.ReadTranxMessage(wsConn, protocol.TranxToSenderReady) 114 | if err != nil { 115 | return err 116 | } 117 | 118 | // PAKE sender -> receiver. 119 | wsConn.WriteJSON(protocol.TranxMessage{ 120 | Type: protocol.SenderToTranxPAKE, 121 | Payload: protocol.PakePayload{ 122 | Bytes: pake.Bytes(), 123 | }, 124 | }) 125 | 126 | // PAKE receiver -> sender. 127 | msg, err = tools.ReadTranxMessage(wsConn, protocol.TranxToSenderPAKE) 128 | if err != nil { 129 | return err 130 | } 131 | 132 | pakePayload := protocol.PakePayload{} 133 | err = tools.DecodePayload(msg.Payload, &pakePayload) 134 | if err != nil { 135 | return err 136 | } 137 | 138 | err = pake.Update(pakePayload.Bytes) 139 | if err != nil { 140 | return err 141 | } 142 | 143 | // Setup crypt.Crypt struct in Sender. 144 | sessionkey, err := pake.SessionKey() 145 | if err != nil { 146 | return err 147 | } 148 | 149 | s.crypt, err = crypt.New(sessionkey) 150 | if err != nil { 151 | return err 152 | } 153 | 154 | // Send salt to receiver. 155 | wsConn.WriteJSON(protocol.TranxMessage{ 156 | Type: protocol.SenderToTranxSalt, 157 | Payload: protocol.SaltPayload{ 158 | Salt: s.crypt.Salt, 159 | }, 160 | }) 161 | 162 | return nil 163 | } 164 | 165 | // doHandshake does the transfer handshake over the tranx connection 166 | func (s *Sender) doHandshake(wsConn *websocket.Conn, payloadReady <-chan bool, startServerCh chan<- ServerOptions) error { 167 | transferMsg, err := tools.ReadEncryptedMessage(wsConn, s.crypt) 168 | if err != nil { 169 | return err 170 | } 171 | 172 | if transferMsg.Type != protocol.ReceiverHandshake { 173 | return protocol.NewWrongMessageTypeError([]protocol.TransferMessageType{protocol.ReceiverHandshake}, transferMsg.Type) 174 | } 175 | 176 | handshakePayload := protocol.ReceiverHandshakePayload{} 177 | err = tools.DecodePayload(transferMsg.Payload, &handshakePayload) 178 | if err != nil { 179 | return err 180 | } 181 | 182 | senderPort, err := tools.GetOpenPort() 183 | if err != nil { 184 | return err 185 | } 186 | 187 | // wait for payload to be ready 188 | <-payloadReady 189 | startServerCh <- ServerOptions{port: senderPort, receiverIP: handshakePayload.IP} 190 | 191 | tcpAddr, _ := wsConn.LocalAddr().(*net.TCPAddr) 192 | handshake := protocol.TransferMessage{ 193 | Type: protocol.SenderHandshake, 194 | Payload: protocol.SenderHandshakePayload{ 195 | IP: tcpAddr.IP, 196 | Port: senderPort, 197 | PayloadSize: s.payloadSize, 198 | }, 199 | } 200 | 201 | tools.WriteEncryptedMessage(wsConn, handshake, s.crypt) 202 | 203 | return nil 204 | } 205 | -------------------------------------------------------------------------------- /core/tranx/handlers.go: -------------------------------------------------------------------------------- 1 | package tranx 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | "encoding/json" 8 | 9 | "github.com/gorilla/websocket" 10 | "github.com/abdfnx/tran/tools" 11 | "github.com/abdfnx/tran/constants" 12 | "github.com/abdfnx/tran/models/protocol" 13 | ) 14 | 15 | // handleEstablishSender returns a websocket handler that communicates with the sender. 16 | func (s *Server) handleEstablishSender() tools.WsHandlerFunc { 17 | return func(wsConn *websocket.Conn) { 18 | // Bind an ID to this communication and send ot to the sender 19 | id := s.ids.Bind() 20 | wsConn.WriteJSON(protocol.TranxMessage{ 21 | Type: protocol.TranxToSenderBind, 22 | Payload: protocol.TranxToSenderBindPayload{ 23 | ID: id, 24 | }, 25 | }) 26 | 27 | msg := protocol.TranxMessage{} 28 | err := wsConn.ReadJSON(&msg) 29 | 30 | if err != nil { 31 | log.Println("message did not follow protocol:", err) 32 | return 33 | } 34 | 35 | if !isExpected(msg.Type, protocol.SenderToTranxEstablish) { 36 | return 37 | } 38 | 39 | // receive the password (hashed) from the sender. 40 | establishPayload := protocol.PasswordPayload{} 41 | err = tools.DecodePayload(msg.Payload, &establishPayload) 42 | if err != nil { 43 | log.Println("error in SenderToTranxEstablish payload:", err) 44 | 45 | return 46 | } 47 | 48 | // Allocate a mailbox for this communication. 49 | mailbox := &Mailbox{ 50 | Sender: &protocol.TranxSender{ 51 | TranxClient: *NewClient(wsConn), 52 | }, 53 | CommunicationChannel: make(chan []byte), 54 | Quit: make(chan bool), 55 | } 56 | 57 | s.mailboxes.StoreMailbox(establishPayload.Password, mailbox) 58 | _, err = s.mailboxes.GetMailbox(establishPayload.Password) 59 | 60 | if err != nil { 61 | log.Println("The created mailbox could not be retrieved") 62 | 63 | return 64 | } 65 | 66 | // wait for receiver to connect 67 | timeout := time.NewTimer(constants.RECEIVER_CONNECT_TIMEOUT) 68 | 69 | select { 70 | case <-timeout.C: 71 | s.ids.Delete(id) 72 | return 73 | 74 | case <-mailbox.CommunicationChannel: 75 | // receiver connected 76 | s.ids.Delete(id) 77 | break 78 | } 79 | 80 | wsConn.WriteJSON(protocol.TranxMessage{ 81 | Type: protocol.TranxToSenderReady, 82 | }) 83 | 84 | msg = protocol.TranxMessage{} 85 | err = wsConn.ReadJSON(&msg) 86 | 87 | if err != nil { 88 | log.Println("message did not follow protocol:", err) 89 | 90 | return 91 | } 92 | 93 | if !isExpected(msg.Type, protocol.SenderToTranxPAKE) { 94 | return 95 | } 96 | 97 | pakePayload := protocol.PakePayload{} 98 | err = tools.DecodePayload(msg.Payload, &pakePayload) 99 | 100 | if err != nil { 101 | log.Println("error in SenderToTranxPAKE payload:", err) 102 | return 103 | } 104 | 105 | // send PAKE bytes to receiver 106 | mailbox.CommunicationChannel <- pakePayload.Bytes 107 | // respond with receiver PAKE bytes 108 | wsConn.WriteJSON(protocol.TranxMessage{ 109 | Type: protocol.TranxToSenderPAKE, 110 | Payload: protocol.PakePayload{ 111 | Bytes: <-mailbox.CommunicationChannel, 112 | }, 113 | }) 114 | 115 | msg = protocol.TranxMessage{} 116 | err = wsConn.ReadJSON(&msg) 117 | 118 | if err != nil { 119 | log.Println("message did not follow protocol:", err) 120 | 121 | return 122 | } 123 | 124 | if !isExpected(msg.Type, protocol.SenderToTranxSalt) { 125 | return 126 | } 127 | 128 | saltPayload := protocol.SaltPayload{} 129 | err = tools.DecodePayload(msg.Payload, &saltPayload) 130 | 131 | if err != nil { 132 | log.Println("error in SenderToTranxSalt payload:", err) 133 | return 134 | } 135 | 136 | // Send the salt to the receiver. 137 | mailbox.CommunicationChannel <- saltPayload.Salt 138 | // Start the relay of messgaes between the sender and receiver handlers. 139 | startRelay(s, wsConn, mailbox, establishPayload.Password) 140 | } 141 | } 142 | 143 | // handleEstablishReceiver returns a websocket handler that that communicates with the sender. 144 | func (s *Server) handleEstablishReceiver() tools.WsHandlerFunc { 145 | return func(wsConn *websocket.Conn) { 146 | // Establish receiver. 147 | msg := protocol.TranxMessage{} 148 | err := wsConn.ReadJSON(&msg) 149 | 150 | if err != nil { 151 | log.Println("message did not follow protocol:", err) 152 | return 153 | } 154 | 155 | if !isExpected(msg.Type, protocol.ReceiverToTranxEstablish) { 156 | return 157 | } 158 | 159 | establishPayload := protocol.PasswordPayload{} 160 | err = tools.DecodePayload(msg.Payload, &establishPayload) 161 | if err != nil { 162 | log.Println("error in ReceiverToTranxEstablish payload:", err) 163 | return 164 | } 165 | 166 | mailbox, err := s.mailboxes.GetMailbox(establishPayload.Password) 167 | 168 | if err != nil { 169 | log.Println("failed to get mailbox:", err) 170 | return 171 | } 172 | 173 | if mailbox.Receiver != nil { 174 | log.Println("mailbox already has a receiver:", err) 175 | return 176 | } 177 | 178 | // this reveiver was first, reserve this mailbox for it to receive 179 | mailbox.Receiver = NewClient(wsConn) 180 | s.mailboxes.StoreMailbox(establishPayload.Password, mailbox) 181 | 182 | // notify sender we are connected 183 | mailbox.CommunicationChannel <- nil 184 | // send back received sender PAKE bytes 185 | wsConn.WriteJSON(protocol.TranxMessage{ 186 | Type: protocol.TranxToReceiverPAKE, 187 | Payload: protocol.PakePayload{ 188 | Bytes: <-mailbox.CommunicationChannel, 189 | }, 190 | }) 191 | 192 | msg = protocol.TranxMessage{} 193 | err = wsConn.ReadJSON(&msg) 194 | 195 | if err != nil { 196 | log.Println("message did not follow protocol:", err) 197 | return 198 | } 199 | 200 | if !isExpected(msg.Type, protocol.ReceiverToTranxPAKE) { 201 | return 202 | } 203 | 204 | receiverPakePayload := protocol.PakePayload{} 205 | err = tools.DecodePayload(msg.Payload, &receiverPakePayload) 206 | 207 | if err != nil { 208 | log.Println("error in ReceiverToTranxPAKE payload:", err) 209 | return 210 | } 211 | 212 | mailbox.CommunicationChannel <- receiverPakePayload.Bytes 213 | wsConn.WriteJSON(protocol.TranxMessage{ 214 | Type: protocol.TranxToReceiverSalt, 215 | Payload: protocol.SaltPayload{ 216 | Salt: <-mailbox.CommunicationChannel, 217 | }, 218 | }) 219 | 220 | startRelay(s, wsConn, mailbox, establishPayload.Password) 221 | } 222 | } 223 | 224 | // starts the relay service, closing it on request (if i.e. clients can communicate directly) 225 | func startRelay(s *Server, wsConn *websocket.Conn, mailbox *Mailbox, mailboxPassword string) { 226 | relayForwardCh := make(chan []byte) 227 | // listen for incoming websocket messages from currently handled client 228 | go func() { 229 | for { 230 | _, p, err := wsConn.ReadMessage() 231 | 232 | if err != nil { 233 | log.Println("error when listening to incoming client messages:", err) 234 | fmt.Printf("closed by: %s\n", wsConn.RemoteAddr()) 235 | mailbox.Quit <- true 236 | 237 | return 238 | } 239 | 240 | relayForwardCh <- p 241 | } 242 | }() 243 | 244 | for { 245 | select { 246 | case relayReceivePayload := <-mailbox.CommunicationChannel: 247 | wsConn.WriteMessage(websocket.BinaryMessage, relayReceivePayload) 248 | 249 | // received payload from __currently handled__ client, relay it to other client 250 | case relayForwardPayload := <-relayForwardCh: 251 | msg := protocol.TranxMessage{} 252 | err := json.Unmarshal(relayForwardPayload, &msg) 253 | // failed to unmarshal, we are in (encrypted) relay-mode, forward message directly to client 254 | if err != nil { 255 | mailbox.CommunicationChannel <- relayForwardPayload 256 | } else { 257 | // close the relay service if sender requested it 258 | if isExpected(msg.Type, protocol.ReceiverToTranxClose) { 259 | mailbox.Quit <- true 260 | 261 | return 262 | } 263 | } 264 | 265 | // deallocate mailbox and quit 266 | case <-mailbox.Quit: 267 | s.mailboxes.Delete(mailboxPassword) 268 | 269 | return 270 | } 271 | } 272 | } 273 | 274 | // isExpected is a convience helper function that checks message types and logs errors. 275 | func isExpected(actual protocol.TranxMessageType, expected protocol.TranxMessageType) bool { 276 | wasExpected := actual == expected 277 | 278 | if !wasExpected { 279 | log.Printf("Expected message of type: %d. Got type %d\n", expected, actual) 280 | } 281 | 282 | return wasExpected 283 | } 284 | -------------------------------------------------------------------------------- /core/tranx/id.go: -------------------------------------------------------------------------------- 1 | package tranx 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // IDs is a threadsafe set of numbers. 8 | type IDs struct{ *sync.Map } 9 | 10 | type void struct{} // empty struct complies to 0 bytes 11 | var member void 12 | 13 | // Bind binds an id to connection. 14 | func (ids *IDs) Bind() int { 15 | id := 1 16 | 17 | for { 18 | val, _ := ids.Load(id) 19 | if val == nil { 20 | break 21 | } 22 | 23 | id++ 24 | } 25 | 26 | ids.Store(id, member) 27 | 28 | return id 29 | } 30 | 31 | // DeleteID Deletes a bound ID. 32 | func (ids *IDs) DeleteID(id int) { 33 | ids.Delete(id) 34 | } 35 | -------------------------------------------------------------------------------- /core/tranx/mailbox.go: -------------------------------------------------------------------------------- 1 | package tranx 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "sync" 7 | 8 | "github.com/gorilla/websocket" 9 | "github.com/abdfnx/tran/models/protocol" 10 | ) 11 | 12 | // Mailbox is a data structure that links together a sender and a receiver client. 13 | type Mailbox struct { 14 | Sender *protocol.TranxSender 15 | Receiver *protocol.TranxReceiver 16 | CommunicationChannel chan []byte 17 | Quit chan bool 18 | } 19 | 20 | type Mailboxes struct{ *sync.Map } 21 | 22 | // StoreMailbox allocates a mailbox. 23 | func (mailboxes *Mailboxes) StoreMailbox(p string, m *Mailbox) { 24 | mailboxes.Store(p, m) 25 | } 26 | 27 | // GetMailbox returns the decired mailbox. 28 | func (mailboxes *Mailboxes) GetMailbox(p string) (*Mailbox, error) { 29 | mailbox, ok := mailboxes.Load(p) 30 | 31 | if !ok { 32 | return nil, fmt.Errorf("no mailbox with password '%s'", p) 33 | } 34 | 35 | return mailbox.(*Mailbox), nil 36 | } 37 | 38 | // DeleteMailbox deallocates a mailbox. 39 | func (mailboxes *Mailboxes) DeleteMailbox(p string) { 40 | mailboxes.Delete(p) 41 | } 42 | 43 | // NewClient returns a new client struct. 44 | func NewClient(wsConn *websocket.Conn) *protocol.TranxClient { 45 | return &protocol.TranxClient{ 46 | Conn: wsConn, 47 | IP: wsConn.RemoteAddr().(*net.TCPAddr).IP, 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /core/tranx/routes.go: -------------------------------------------------------------------------------- 1 | package tranx 2 | 3 | import "github.com/abdfnx/tran/tools" 4 | 5 | func (s *Server) routes() { 6 | s.router.HandleFunc("/establish-sender", tools.WebsocketHandler(s.handleEstablishSender())) 7 | s.router.HandleFunc("/establish-receiver", tools.WebsocketHandler(s.handleEstablishReceiver())) 8 | } 9 | -------------------------------------------------------------------------------- /core/tranx/server.go: -------------------------------------------------------------------------------- 1 | package tranx 2 | 3 | import ( 4 | "os" 5 | "fmt" 6 | "log" 7 | "sync" 8 | "time" 9 | "context" 10 | "net/http" 11 | ) 12 | 13 | // Server is contains the necessary data to run the tranx server. 14 | type Server struct { 15 | httpServer *http.Server 16 | router *http.ServeMux 17 | mailboxes *Mailboxes 18 | ids *IDs 19 | signal chan os.Signal 20 | } 21 | 22 | // NewServer constructs a new Server struct and setups the routes. 23 | func NewServer(port int) *Server { 24 | router := &http.ServeMux{} 25 | 26 | s := &Server{ 27 | httpServer: &http.Server{ 28 | Addr: fmt.Sprintf(":%d", port), 29 | ReadTimeout: 30 * time.Second, 30 | WriteTimeout: 30 * time.Second, 31 | Handler: router, 32 | }, 33 | router: router, 34 | mailboxes: &Mailboxes{&sync.Map{}}, 35 | ids: &IDs{&sync.Map{}}, 36 | } 37 | 38 | s.routes() 39 | 40 | return s 41 | } 42 | 43 | // Start runs the tranx server. 44 | func (s *Server) Start() { 45 | ctx, cancel := context.WithCancel(context.Background()) 46 | 47 | go func() { 48 | <-s.signal 49 | cancel() 50 | }() 51 | 52 | if err := serve(s, ctx); err != nil { 53 | log.Printf("Error serving Tran tranx server: %s\n", err) 54 | } 55 | } 56 | 57 | // serve is a helper function providing graceful shutdown of the server. 58 | func serve(s *Server, ctx context.Context) (err error) { 59 | go func() { 60 | if err = s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { 61 | log.Fatalf("Serving Tran: %s\n", err) 62 | } 63 | }() 64 | 65 | log.Printf("Tran Tranx Server started at \"%s\" \n", s.httpServer.Addr) 66 | <-ctx.Done() 67 | 68 | ctxShutdown, cancel := context.WithTimeout(context.Background(), 5*time.Second) 69 | defer func() { 70 | cancel() 71 | }() 72 | 73 | if err = s.httpServer.Shutdown(ctxShutdown); err != nil { 74 | log.Fatalf("Tran tranx shutdown failed: %s", err) 75 | } 76 | 77 | if err == http.ErrServerClosed { 78 | err = nil 79 | } 80 | 81 | return err 82 | } 83 | -------------------------------------------------------------------------------- /data/password-codes.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | var PasswordList = []string{ 4 | "go", 5 | "rust", 6 | "solar", 7 | "moon", 8 | "cli", 9 | "planet", 10 | "byte", 11 | "comet", 12 | "star", 13 | "orbit", 14 | "galaxy", 15 | "lunar", 16 | "vue", 17 | "meteor", 18 | "mass", 19 | "football", 20 | "nebula", 21 | "orbit", 22 | "ios", 23 | "lightyear", 24 | "parsec", 25 | "neutron", 26 | "supernova", 27 | "quasar", 28 | "cluster", 29 | "redshift", 30 | "gamma", 31 | "flare", 32 | "ray", 33 | "tidal", 34 | "xcode", 35 | "hub", 36 | "compose", 37 | "docker", 38 | "kubernetes", 39 | "computer", 40 | "cloud", 41 | "react", 42 | "secman", 43 | "x", 44 | "base", 45 | } 46 | -------------------------------------------------------------------------------- /dfs/directory-file-system.go: -------------------------------------------------------------------------------- 1 | package dfs 2 | 3 | import ( 4 | "os" 5 | "fmt" 6 | "io/fs" 7 | "errors" 8 | "strings" 9 | "path/filepath" 10 | ) 11 | 12 | // Directory shortcuts. 13 | const ( 14 | CurrentDirectory = "." 15 | PreviousDirectory = ".." 16 | HomeDirectory = "~" 17 | RootDirectory = "/" 18 | ) 19 | 20 | // Different types of listings. 21 | const ( 22 | DirectoriesListingType = "directories" 23 | FilesListingType = "files" 24 | ) 25 | 26 | // RenameDirectoryItem renames a directory or files given a source and destination. 27 | func RenameDirectoryItem(src, dst string) error { 28 | err := os.Rename(src, dst) 29 | 30 | return err 31 | } 32 | 33 | // CreateDirectory creates a new directory given a name. 34 | func CreateDirectory(name string) error { 35 | if _, err := os.Stat(name); errors.Is(err, os.ErrNotExist) { 36 | err := os.Mkdir(name, os.ModePerm) 37 | if err != nil { 38 | return err 39 | } 40 | } 41 | 42 | return nil 43 | } 44 | 45 | // GetDirectoryListing returns a list of files and directories within a given directory. 46 | func GetDirectoryListing(dir string, showHidden bool) ([]fs.DirEntry, error) { 47 | n := 0 48 | 49 | files, err := os.ReadDir(dir) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | if !showHidden { 55 | for _, file := range files { 56 | // If the file or directory starts with a dot, 57 | // we know its hidden so dont add it to the array 58 | // of files to return. 59 | if !strings.HasPrefix(file.Name(), ".") { 60 | files[n] = file 61 | n++ 62 | } 63 | } 64 | 65 | // Set files to the list that does not include hidden files. 66 | files = files[:n] 67 | } 68 | 69 | return files, nil 70 | } 71 | 72 | // GetDirectoryListingByType returns a directory listing based on type (directories | files). 73 | func GetDirectoryListingByType(dir, listingType string, showHidden bool) ([]fs.DirEntry, error) { 74 | n := 0 75 | 76 | files, err := os.ReadDir(dir) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | for _, file := range files { 82 | switch { 83 | case file.IsDir() && listingType == DirectoriesListingType && !showHidden: 84 | if !strings.HasPrefix(file.Name(), ".") { 85 | files[n] = file 86 | n++ 87 | } 88 | 89 | case file.IsDir() && listingType == DirectoriesListingType && showHidden: 90 | files[n] = file 91 | n++ 92 | 93 | case !file.IsDir() && listingType == FilesListingType && !showHidden: 94 | if !strings.HasPrefix(file.Name(), ".") { 95 | files[n] = file 96 | n++ 97 | } 98 | 99 | case !file.IsDir() && listingType == FilesListingType && showHidden: 100 | files[n] = file 101 | n++ 102 | } 103 | } 104 | 105 | return files[:n], nil 106 | } 107 | 108 | // DeleteDirectory deletes a directory given a name. 109 | func DeleteDirectory(name string) error { 110 | err := os.RemoveAll(name) 111 | 112 | return err 113 | } 114 | 115 | // GetHomeDirectory returns the users home directory. 116 | func GetHomeDirectory() (string, error) { 117 | home, err := os.UserHomeDir() 118 | if err != nil { 119 | return "", err 120 | } 121 | 122 | return home, nil 123 | } 124 | 125 | // GetWorkingDirectory returns the current working directory. 126 | func GetWorkingDirectory() (string, error) { 127 | workingDir, err := os.Getwd() 128 | if err != nil { 129 | return "", err 130 | } 131 | 132 | return workingDir, nil 133 | } 134 | 135 | // DeleteFile deletes a file given a name. 136 | func DeleteFile(name string) error { 137 | err := os.Remove(name) 138 | 139 | return err 140 | } 141 | 142 | // MoveDirectoryItem moves a file from one place to another. 143 | func MoveDirectoryItem(src, dst string) error { 144 | err := os.Rename(src, dst) 145 | 146 | return err 147 | } 148 | 149 | // ReadFileContent returns the contents of a file given a name. 150 | func ReadFileContent(name string) (string, error) { 151 | fileContent, err := os.ReadFile(name) 152 | if err != nil { 153 | return "", err 154 | } 155 | 156 | return string(fileContent), nil 157 | } 158 | 159 | // GetDirectoryItemSize calculates the size of a directory or file. 160 | func GetDirectoryItemSize(path string) (int64, error) { 161 | curFile, err := os.Stat(path) 162 | if err != nil { 163 | return 0, err 164 | } 165 | 166 | if !curFile.IsDir() { 167 | return curFile.Size(), nil 168 | } 169 | 170 | var size int64 171 | 172 | err = filepath.WalkDir(path, func(path string, d os.DirEntry, err error) error { 173 | if err != nil { 174 | return err 175 | } 176 | 177 | fileInfo, err := d.Info() 178 | if err != nil { 179 | return err 180 | } 181 | 182 | if !d.IsDir() { 183 | size += fileInfo.Size() 184 | } 185 | 186 | return err 187 | }) 188 | 189 | return size, err 190 | } 191 | 192 | func FindFilesByName(name, dir string) ([]string, []fs.DirEntry, error) { 193 | var paths []string 194 | var entries []fs.DirEntry 195 | 196 | err := filepath.WalkDir(dir, func(path string, entry os.DirEntry, err error) error { 197 | if err != nil { 198 | return filepath.SkipDir 199 | } 200 | 201 | if strings.Contains(entry.Name(), name) { 202 | paths = append(paths, path) 203 | entries = append(entries, entry) 204 | } 205 | 206 | return err 207 | }) 208 | 209 | return paths, entries, err 210 | } 211 | 212 | // WriteToFile writes content to a file, overwriting content if it exists. 213 | func WriteToFile(path, content string) error { 214 | f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.ModePerm) 215 | if err != nil { 216 | return err 217 | } 218 | 219 | workingDir, err := os.Getwd() 220 | if err != nil { 221 | return err 222 | } 223 | 224 | _, err = f.WriteString(fmt.Sprintf("%s\n", filepath.Join(workingDir, content))) 225 | 226 | if err != nil { 227 | f.Close() 228 | return err 229 | } 230 | 231 | err = f.Close() 232 | if err != nil { 233 | return err 234 | } 235 | 236 | return err 237 | } 238 | -------------------------------------------------------------------------------- /docker/container/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:latest 2 | 3 | ### variables ### 4 | ARG UPD="apt-get update" 5 | ARG UPD_s="sudo $UPD" 6 | ARG INS="apt-get install" 7 | ARG INS_s="sudo $INS" 8 | ENV PKGS="zip unzip multitail curl lsof wget ssl-cert asciidoctor apt-transport-https ca-certificates gnupg-agent bash-completion build-essential htop jq software-properties-common less llvm locales man-db nano vim ruby-full build-essential zlib1g-dev libncurses5-dev libgdbm-dev libnss3-dev libssl-dev libsqlite3-dev libreadline-dev libffi-dev libbz2-dev" 9 | 10 | RUN $UPD && $INS -y $PKGS && $UPD && \ 11 | locale-gen en_US.UTF-8 && \ 12 | mkdir /var/lib/apt/abdcodedoc-marks && \ 13 | apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* && \ 14 | $UPD 15 | 16 | ENV LANG=en_US.UTF-8 17 | 18 | ### git ### 19 | RUN $INS -y git && \ 20 | rm -rf /var/lib/apt/lists/* && \ 21 | $UPD 22 | 23 | ### sudo ### 24 | RUN $UPD && $INS -y sudo && \ 25 | adduser --disabled-password --gecos '' trn && \ 26 | adduser trn sudo && \ 27 | echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers 28 | 29 | ENV HOME="/home/trn" 30 | WORKDIR $HOME 31 | USER trn 32 | 33 | ### go ### 34 | COPY --from=golang /usr/local/go/ /usr/local/go/ 35 | ENV PATH="/usr/local/go/bin:${PATH}" 36 | 37 | ### tran ### 38 | RUN curl -sL https://cutt.ly/tran-cli | bash 39 | 40 | ### zsh ### 41 | ENV src=".zshrc" 42 | 43 | RUN $INS_s zsh -y 44 | RUN zsh && \ 45 | sh -c "$(curl -fsSL https://raw.github.com/robbyrussell/oh-my-zsh/master/tools/install.sh)" && \ 46 | $UPD_s && \ 47 | git clone https://github.com/zsh-users/zsh-syntax-highlighting ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting && \ 48 | git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions 49 | 50 | ### rm old ~/.zshrc ### 51 | RUN sudo rm -rf $src 52 | 53 | ### wget new files ### 54 | RUN wget https://abdfnx.github.io/tran/scripts/shell/zshrc -o $src 55 | RUN wget https://abdfnx.github.io/tran/scripts/shell/README 56 | 57 | CMD /bin/bash -c "cat README && zsh" 58 | -------------------------------------------------------------------------------- /docker/dev/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:latest 2 | 3 | ### variables ### 4 | ARG UPD="apt-get update" 5 | ARG UPD_s="sudo $UPD" 6 | ARG INS="apt-get install" 7 | ARG INS_s="sudo $INS" 8 | 9 | RUN $UPD && $INS -y build-essential software-properties-common && $UPD && \ 10 | locale-gen en_US.UTF-8 && \ 11 | mkdir /var/lib/apt/abdcodedoc-marks && \ 12 | apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* && \ 13 | $UPD 14 | 15 | ENV LANG=en_US.UTF-8 16 | 17 | ### git ### 18 | RUN $INS -y git && \ 19 | rm -rf /var/lib/apt/lists/* && \ 20 | $UPD 21 | 22 | ### sudo ### 23 | RUN $UPD && $INS -y sudo && \ 24 | adduser --disabled-password --gecos '' trn && \ 25 | adduser trn sudo && \ 26 | echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers 27 | 28 | ENV HOME="/home/trn" 29 | WORKDIR $HOME 30 | USER trn 31 | 32 | ### go ### 33 | COPY --from=golang /usr/local/go/ /usr/local/go/ 34 | ENV PATH="/usr/local/go/bin:${PATH}" 35 | 36 | ### tran ### 37 | RUN go install github.com/abdfnx/tran@latest 38 | -------------------------------------------------------------------------------- /docker/vm/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | 3 | RUN apk update && apk upgrade && apk add --no-cache ca-certificates 4 | 5 | COPY tran /usr/bin/tran 6 | 7 | ENTRYPOINT ["/usr/bin/tran"] 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/abdfnx/tran 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/AlecAivazis/survey/v2 v2.3.7 7 | github.com/MakeNowJust/heredoc v1.0.0 8 | github.com/abdfnx/gh v0.1.5 9 | github.com/abdfnx/gosh v0.4.0 10 | github.com/abdfnx/looker v0.1.0 11 | github.com/abdfnx/resto v0.1.6 12 | github.com/alecthomas/chroma v0.10.0 13 | github.com/briandowns/spinner v1.23.0 14 | github.com/charmbracelet/bubbles v0.18.0 15 | github.com/charmbracelet/bubbletea v0.25.0 16 | github.com/charmbracelet/glamour v0.7.0 17 | github.com/charmbracelet/lipgloss v0.10.0 18 | github.com/david-tomson/tran-git v0.0.3 19 | github.com/disintegration/imaging v1.6.2 20 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 21 | github.com/gorilla/websocket v1.5.1 22 | github.com/klauspost/pgzip v1.2.6 23 | github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 24 | github.com/lucasb-eyer/go-colorful v1.2.0 25 | github.com/mattn/go-colorable v0.1.13 26 | github.com/mattn/go-isatty v0.0.20 27 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d 28 | github.com/muesli/reflow v0.3.0 29 | github.com/muesli/termenv v0.15.2 30 | github.com/schollz/pake/v3 v3.0.5 31 | github.com/spf13/cobra v1.8.0 32 | github.com/spf13/pflag v1.0.5 33 | github.com/spf13/viper v1.18.2 34 | github.com/tidwall/gjson v1.17.1 35 | golang.org/x/crypto v0.21.0 36 | golang.org/x/sys v0.19.0 37 | golang.org/x/term v0.18.0 38 | ) 39 | 40 | require ( 41 | github.com/alecthomas/chroma/v2 v2.8.0 // indirect 42 | github.com/atotto/clipboard v0.1.4 // indirect 43 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 44 | github.com/aymerick/douceur v0.2.0 // indirect 45 | github.com/charmbracelet/harmonica v0.2.0 // indirect 46 | github.com/cli/browser v1.1.0 // indirect 47 | github.com/cli/oauth v0.9.0 // indirect 48 | github.com/cli/safeexec v1.0.0 // indirect 49 | github.com/cli/shurcooL-graphql v0.0.1 // indirect 50 | github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect 51 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 52 | github.com/dlclark/regexp2 v1.7.0 // indirect 53 | github.com/fatih/color v1.14.1 // indirect 54 | github.com/fsnotify/fsnotify v1.7.0 // indirect 55 | github.com/gorilla/css v1.0.0 // indirect 56 | github.com/hashicorp/hcl v1.0.0 // indirect 57 | github.com/henvic/httpretty v0.1.0 // indirect 58 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 59 | github.com/itchyny/gojq v0.12.8 // indirect 60 | github.com/itchyny/timefmt-go v0.1.3 // indirect 61 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect 62 | github.com/klauspost/compress v1.17.0 // indirect 63 | github.com/magiconair/properties v1.8.7 // indirect 64 | github.com/mattn/go-localereader v0.0.1 // indirect 65 | github.com/mattn/go-runewidth v0.0.15 // indirect 66 | github.com/microcosm-cc/bluemonday v1.0.25 // indirect 67 | github.com/mitchellh/mapstructure v1.5.0 // indirect 68 | github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect 69 | github.com/muesli/cancelreader v0.2.2 // indirect 70 | github.com/olekukonko/tablewriter v0.0.5 // indirect 71 | github.com/pelletier/go-toml/v2 v2.1.0 // indirect 72 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 73 | github.com/rivo/uniseg v0.4.7 // indirect 74 | github.com/sagikazarmark/locafero v0.4.0 // indirect 75 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 76 | github.com/scmn-dev/browser v0.1.3 // indirect 77 | github.com/shurcooL/githubv4 v0.0.0-20220520033151-0b4e3294ff00 // indirect 78 | github.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29 // indirect 79 | github.com/sourcegraph/conc v0.3.0 // indirect 80 | github.com/spf13/afero v1.11.0 // indirect 81 | github.com/spf13/cast v1.6.0 // indirect 82 | github.com/stretchr/objx v0.5.0 // indirect 83 | github.com/stretchr/testify v1.8.4 // indirect 84 | github.com/subosito/gotenv v1.6.0 // indirect 85 | github.com/tidwall/match v1.1.1 // indirect 86 | github.com/tidwall/pretty v1.2.0 // indirect 87 | github.com/tscholl2/siec v0.0.0-20210707234609-9bdfc483d499 // indirect 88 | github.com/yuin/goldmark v1.5.4 // indirect 89 | github.com/yuin/goldmark-emoji v1.0.2 // indirect 90 | go.uber.org/atomic v1.9.0 // indirect 91 | go.uber.org/multierr v1.9.0 // indirect 92 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect 93 | golang.org/x/image v0.0.0-20220722155232-062f8c9fd539 // indirect 94 | golang.org/x/net v0.21.0 // indirect 95 | golang.org/x/sync v0.5.0 // indirect 96 | golang.org/x/text v0.14.0 // indirect 97 | gopkg.in/ini.v1 v1.67.0 // indirect 98 | gopkg.in/yaml.v3 v3.0.1 // indirect 99 | ) 100 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "log" 6 | "runtime" 7 | "path/filepath" 8 | 9 | "github.com/spf13/pflag" 10 | "github.com/spf13/viper" 11 | "github.com/abdfnx/tran/dfs" 12 | ) 13 | 14 | // TranConfig struct represents the config for the config. 15 | type TranConfig struct { 16 | StartDir string `mapstructure:"start_dir"` 17 | Borderless bool `mapstructure:"borderless"` 18 | Editor string `mapstructure:"editor"` 19 | EnableMouseWheel bool `mapstructure:"enable_mousewheel"` 20 | ShowUpdates bool `mapstructure:"show_updates"` 21 | } 22 | 23 | // Config represents the main config for the application. 24 | type Config struct { 25 | Tran TranConfig `mapstructure:"config"` 26 | } 27 | 28 | func defualtEditor() string { 29 | if runtime.GOOS == "windows" { 30 | return "notepad.exe" 31 | } 32 | 33 | return "vim" 34 | } 35 | 36 | // LoadConfig loads a users config and creates the config if it does not exist 37 | // located at `~/.tran/tran.yml` 38 | func LoadConfig(startDir *pflag.Flag) { 39 | var err error 40 | 41 | homeDir, err := dfs.GetHomeDirectory() 42 | 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | 47 | err = dfs.CreateDirectory(filepath.Join(homeDir, ".tran")) 48 | if err != nil { 49 | log.Fatal(err) 50 | } 51 | 52 | // Windows doesn't use the $HOME env variable, 53 | // check if running on Windows and if so, use $USERPROFILE 54 | if runtime.GOOS == "windows" { 55 | viper.AddConfigPath(`$USERPROFILE\\.tran`) 56 | } else { 57 | viper.AddConfigPath("$HOME/.tran") 58 | } 59 | 60 | viper.SetConfigName("tran") 61 | viper.SetConfigType("yml") 62 | 63 | // Setup config defaults. 64 | viper.SetDefault("config.start_dir", ".") 65 | viper.SetDefault("config.enable_mousewheel", true) 66 | viper.SetDefault("config.borderless", false) 67 | viper.SetDefault("config.editor", defualtEditor()) 68 | viper.SetDefault("config.show_updates", true) 69 | 70 | if err := viper.SafeWriteConfig(); err != nil { 71 | if os.IsNotExist(err) { 72 | err = viper.WriteConfig() 73 | 74 | if err != nil { 75 | log.Fatal(err) 76 | } 77 | } 78 | } 79 | 80 | if err := viper.ReadInConfig(); err != nil { 81 | if _, ok := err.(viper.ConfigFileNotFoundError); ok { 82 | log.Fatal(err) 83 | } 84 | } 85 | 86 | // Setup flags. 87 | err = viper.BindPFlag("start-dir", startDir) 88 | if err != nil { 89 | log.Fatal(err) 90 | } 91 | 92 | // Setup flag defaults. 93 | viper.SetDefault("start-dir", "") 94 | } 95 | 96 | // GetConfig returns the users config. 97 | func GetConfig() (config Config) { 98 | if err := viper.Unmarshal(&config); err != nil { 99 | log.Fatal("Error parsing config", err) 100 | } 101 | 102 | return 103 | } 104 | -------------------------------------------------------------------------------- /internal/theme/theme.go: -------------------------------------------------------------------------------- 1 | package theme 2 | 3 | import ( 4 | "github.com/abdfnx/tran/constants" 5 | "github.com/charmbracelet/lipgloss" 6 | ) 7 | 8 | type Theme struct { 9 | SelectedTreeItemColor lipgloss.AdaptiveColor 10 | UnselectedTreeItemColor lipgloss.AdaptiveColor 11 | ActiveBoxBorderColor lipgloss.AdaptiveColor 12 | InactiveBoxBorderColor lipgloss.AdaptiveColor 13 | SpinnerColor lipgloss.AdaptiveColor 14 | StatusBarSelectedFileForegroundColor lipgloss.AdaptiveColor 15 | StatusBarSelectedFileBackgroundColor lipgloss.AdaptiveColor 16 | StatusBarBarForegroundColor lipgloss.AdaptiveColor 17 | StatusBarBarBackgroundColor lipgloss.AdaptiveColor 18 | StatusBarTotalFilesForegroundColor lipgloss.AdaptiveColor 19 | StatusBarTotalFilesBackgroundColor lipgloss.AdaptiveColor 20 | StatusBarLogoForegroundColor lipgloss.AdaptiveColor 21 | StatusBarLogoBackgroundColor lipgloss.AdaptiveColor 22 | ErrorColor lipgloss.AdaptiveColor 23 | DefaultTextColor lipgloss.AdaptiveColor 24 | } 25 | 26 | // appColors contains the different types of colors. 27 | type appColors struct { 28 | white string 29 | darkGray string 30 | red string 31 | black string 32 | } 33 | 34 | // Colors contains the different kinds of colors and their values. 35 | var colors = appColors{ 36 | white: "#FFFDF5", 37 | darkGray: constants.DARK_GRAY_COLOR, 38 | red: "#cc241d", 39 | black: "#000000", 40 | } 41 | 42 | // themeMap represents the mapping of different themes. 43 | var themeMap = map[string]Theme{ 44 | "default": { 45 | SelectedTreeItemColor: lipgloss.AdaptiveColor{Dark: constants.PRIMARY_COLOR, Light: constants.PRIMARY_COLOR}, 46 | UnselectedTreeItemColor: lipgloss.AdaptiveColor{Dark: colors.white, Light: colors.black}, 47 | ActiveBoxBorderColor: lipgloss.AdaptiveColor{Dark: constants.PRIMARY_COLOR, Light: constants.PRIMARY_COLOR}, 48 | InactiveBoxBorderColor: lipgloss.AdaptiveColor{Dark: colors.white, Light: colors.black}, 49 | SpinnerColor: lipgloss.AdaptiveColor{Dark: constants.PRIMARY_COLOR, Light: constants.PRIMARY_COLOR}, 50 | StatusBarSelectedFileForegroundColor: lipgloss.AdaptiveColor{Dark: colors.white, Light: colors.white}, 51 | StatusBarSelectedFileBackgroundColor: lipgloss.AdaptiveColor{Dark: "#4880EC", Light: "#4880EC"}, 52 | StatusBarBarForegroundColor: lipgloss.AdaptiveColor{Dark: colors.white, Light: colors.white}, 53 | StatusBarBarBackgroundColor: lipgloss.AdaptiveColor{Dark: colors.darkGray, Light: colors.darkGray}, 54 | StatusBarTotalFilesForegroundColor: lipgloss.AdaptiveColor{Dark: colors.white, Light: colors.white}, 55 | StatusBarTotalFilesBackgroundColor: lipgloss.AdaptiveColor{Dark: "#1E6AFF", Light: "#1E6AFF"}, 56 | StatusBarLogoForegroundColor: lipgloss.AdaptiveColor{Dark: colors.white, Light: colors.white}, 57 | StatusBarLogoBackgroundColor: lipgloss.AdaptiveColor{Dark: "#1747A6", Light: "#1747A6"}, 58 | ErrorColor: lipgloss.AdaptiveColor{Dark: colors.red, Light: colors.red}, 59 | DefaultTextColor: lipgloss.AdaptiveColor{Dark: colors.white, Light: colors.black}, 60 | }, 61 | } 62 | 63 | // GetTheme returns a theme based on the given name. 64 | func GetTheme(theme string) Theme { 65 | return themeMap["default"] 66 | } 67 | -------------------------------------------------------------------------------- /internal/tui/cmds.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "os" 5 | "image" 6 | "io/fs" 7 | _ "image/png" 8 | _ "image/jpeg" 9 | "path/filepath" 10 | 11 | "github.com/abdfnx/tran/dfs" 12 | "github.com/abdfnx/tran/models" 13 | "github.com/abdfnx/tran/renderer" 14 | "github.com/abdfnx/tran/constants" 15 | "github.com/charmbracelet/lipgloss" 16 | tea "github.com/charmbracelet/bubbletea" 17 | ) 18 | 19 | type updateDirectoryListingMsg []fs.DirEntry 20 | type viewDirectoryListingMsg []fs.DirEntry 21 | type errorMsg string 22 | type convertImageToStringMsg string 23 | type directoryItemSizeMsg struct { 24 | index int 25 | size string 26 | } 27 | 28 | type findFilesByNameMsg struct { 29 | paths []string 30 | entries []fs.DirEntry 31 | } 32 | 33 | type readFileContentMsg struct { 34 | rawContent string 35 | markdown string 36 | code string 37 | imageString string 38 | pdfContent string 39 | image image.Image 40 | } 41 | 42 | // updateDirectoryListingCmd updates the directory listing based on the name of the directory provided. 43 | func (b Bubble) updateDirectoryListingCmd(name string) tea.Cmd { 44 | return func() tea.Msg { 45 | files, err := dfs.GetDirectoryListing(name, b.showHiddenFiles) 46 | if err != nil { 47 | return errorMsg(err.Error()) 48 | } 49 | 50 | err = os.Chdir(name) 51 | if err != nil { 52 | return errorMsg(err.Error()) 53 | } 54 | 55 | return updateDirectoryListingMsg(files) 56 | } 57 | } 58 | 59 | // viewDirectoryListingCmd updates the directory listing based on the name of the directory provided. 60 | func (b Bubble) viewDirectoryListingCmd(name string) tea.Cmd { 61 | return func() tea.Msg { 62 | currentDir, err := dfs.GetWorkingDirectory() 63 | if err != nil { 64 | return errorMsg(err.Error()) 65 | } 66 | 67 | files, err := dfs.GetDirectoryListing(filepath.Join(currentDir, name), b.showHiddenFiles) 68 | if err != nil { 69 | return errorMsg(err.Error()) 70 | } 71 | 72 | return viewDirectoryListingMsg(files) 73 | } 74 | } 75 | 76 | // convertImageToStringCmd redraws the image based on the width provided. 77 | func (b Bubble) convertImageToStringCmd(width int) tea.Cmd { 78 | return func() tea.Msg { 79 | imageString := renderer.ImageToString(width, b.currentImage) 80 | 81 | return convertImageToStringMsg(imageString) 82 | } 83 | } 84 | 85 | // readFileContentCmd reads the content of a file and returns it. 86 | func (b Bubble) readFileContentCmd(fileName string, width int) tea.Cmd { 87 | return func() tea.Msg { 88 | content, err := dfs.ReadFileContent(fileName) 89 | 90 | if err != nil { 91 | return errorMsg(err.Error()) 92 | } 93 | 94 | switch { 95 | case filepath.Ext(fileName) == ".md": 96 | markdownContent, err := renderer.RenderMarkdown(width, content) 97 | 98 | if err != nil { 99 | return errorMsg(err.Error()) 100 | } 101 | 102 | return readFileContentMsg{ 103 | rawContent: content, 104 | markdown: markdownContent, 105 | code: "", 106 | imageString: "", 107 | pdfContent: "", 108 | image: nil, 109 | } 110 | 111 | case filepath.Ext(fileName) == ".png" || filepath.Ext(fileName) == ".jpg" || filepath.Ext(fileName) == ".jpeg": 112 | imageContent, err := os.Open(fileName) 113 | 114 | if err != nil { 115 | return errorMsg(err.Error()) 116 | } 117 | 118 | img, _, err := image.Decode(imageContent) 119 | 120 | if err != nil { 121 | return errorMsg(err.Error()) 122 | } 123 | 124 | imageString := renderer.ImageToString(width, img) 125 | 126 | return readFileContentMsg{ 127 | rawContent: content, 128 | code: "", 129 | markdown: "", 130 | imageString: imageString, 131 | pdfContent: "", 132 | image: img, 133 | } 134 | 135 | case filepath.Ext(fileName) == ".pdf": 136 | pdfContent, err := renderer.ReadPdf(fileName) 137 | if err != nil { 138 | return errorMsg(err.Error()) 139 | } 140 | 141 | return readFileContentMsg{ 142 | rawContent: content, 143 | code: "", 144 | markdown: "", 145 | imageString: "", 146 | pdfContent: pdfContent, 147 | image: nil, 148 | } 149 | 150 | default: 151 | syntaxTheme := "solarized-light" 152 | 153 | if lipgloss.HasDarkBackground() { 154 | syntaxTheme = "solarized-dark" 155 | } 156 | 157 | code, err := renderer.Highlight(content, filepath.Ext(fileName), syntaxTheme) 158 | 159 | if err != nil { 160 | return errorMsg(err.Error()) 161 | } 162 | 163 | return readFileContentMsg{ 164 | rawContent: content, 165 | code: code, 166 | markdown: "", 167 | imageString: "", 168 | pdfContent: "", 169 | image: nil, 170 | } 171 | } 172 | } 173 | } 174 | 175 | // getDirectoryItemSizeCmd calculates the size of a directory or file. 176 | func (b Bubble) getDirectoryItemSizeCmd(name string, i int) tea.Cmd { 177 | return func() tea.Msg { 178 | size, err := dfs.GetDirectoryItemSize(name) 179 | 180 | if err != nil { 181 | return directoryItemSizeMsg{size: "N/A", index: i} 182 | } 183 | 184 | sizeString := renderer.ConvertBytesToSizeString(size) 185 | 186 | return directoryItemSizeMsg{ 187 | size: sizeString, 188 | index: i, 189 | } 190 | } 191 | } 192 | 193 | // handleErrorCmd returns an error message to the UI. 194 | func (b Bubble) handleErrorCmd(err error) tea.Cmd { 195 | return func() tea.Msg { 196 | return errorMsg(err.Error()) 197 | } 198 | } 199 | 200 | // getDirectoryListingByTypeCmd returns only directories in the current directory. 201 | func (b Bubble) getDirectoryListingByTypeCmd(listType string) tea.Cmd { 202 | return func() tea.Msg { 203 | workingDir, err := dfs.GetWorkingDirectory() 204 | if err != nil { 205 | return errorMsg(err.Error()) 206 | } 207 | 208 | directories, err := dfs.GetDirectoryListingByType(workingDir, listType, b.showHiddenFiles) 209 | if err != nil { 210 | return errorMsg(err.Error()) 211 | } 212 | 213 | return updateDirectoryListingMsg(directories) 214 | } 215 | } 216 | 217 | // findFilesByNameCmd finds files based on name. 218 | func (b Bubble) findFilesByNameCmd(name string) tea.Cmd { 219 | return func() tea.Msg { 220 | workingDir, err := dfs.GetWorkingDirectory() 221 | if err != nil { 222 | return errorMsg(err.Error()) 223 | } 224 | 225 | paths, entries, err := dfs.FindFilesByName(name, workingDir) 226 | if err != nil { 227 | return errorMsg(err.Error()) 228 | } 229 | 230 | return findFilesByNameMsg{ 231 | paths: paths, 232 | entries: entries, 233 | } 234 | } 235 | } 236 | 237 | func (b Bubble) receiveFileCmd(password string) tea.Cmd { 238 | return func() tea.Msg { 239 | err := ValidateTranxAddress() 240 | 241 | if err != nil { 242 | return err 243 | } 244 | 245 | HandleReceiveCommand(models.TranOptions{ 246 | TranxAddress: constants.DEFAULT_ADDRESS, 247 | TranxPort: constants.DEFAULT_PORT, 248 | }, password) 249 | 250 | return nil 251 | } 252 | } 253 | 254 | // redrawCmd redraws the UI. 255 | func (b Bubble) redrawCmd() tea.Cmd { 256 | return func() tea.Msg { 257 | return tea.WindowSizeMsg{ 258 | Width: b.width, 259 | Height: b.height, 260 | } 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /internal/tui/init.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "os" 5 | "log" 6 | "strings" 7 | "path/filepath" 8 | 9 | "github.com/spf13/viper" 10 | "github.com/abdfnx/tran/dfs" 11 | tea "github.com/charmbracelet/bubbletea" 12 | "github.com/charmbracelet/bubbles/spinner" 13 | "github.com/charmbracelet/bubbles/textinput" 14 | ) 15 | 16 | // Init initializes the UI and sets up initial data. 17 | func (b Bubble) Init() tea.Cmd { 18 | var cmds []tea.Cmd 19 | startDir := viper.GetString("start-dir") 20 | 21 | switch { 22 | case startDir != "": 23 | _, err := os.Stat(startDir) 24 | if err != nil { 25 | return nil 26 | } 27 | 28 | if strings.HasPrefix(startDir, dfs.RootDirectory) { 29 | cmds = append(cmds, b.updateDirectoryListingCmd(startDir)) 30 | } else { 31 | path, err := os.Getwd() 32 | 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | 37 | filePath := filepath.Join(path, startDir) 38 | 39 | cmds = append(cmds, b.updateDirectoryListingCmd(filePath)) 40 | } 41 | 42 | case b.appConfig.Tran.StartDir == dfs.HomeDirectory: 43 | homeDir, err := dfs.GetHomeDirectory() 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | 48 | cmds = append(cmds, b.updateDirectoryListingCmd(homeDir)) 49 | 50 | default: 51 | cmds = append(cmds, b.updateDirectoryListingCmd(b.appConfig.Tran.StartDir)) 52 | } 53 | 54 | cmds = append(cmds, spinner.Tick) 55 | cmds = append(cmds, textinput.Blink) 56 | 57 | return tea.Batch(cmds...) 58 | } 59 | -------------------------------------------------------------------------------- /internal/tui/keymap.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import "github.com/charmbracelet/bubbles/key" 4 | 5 | type KeyMap struct { 6 | Quit key.Binding 7 | Down key.Binding 8 | Up key.Binding 9 | Left key.Binding 10 | Right key.Binding 11 | View key.Binding 12 | Receive key.Binding 13 | GotoBottom key.Binding 14 | HomeShortcut key.Binding 15 | RootShortcut key.Binding 16 | ToggleHidden key.Binding 17 | ShowDirectoriesOnly key.Binding 18 | ShowFilesOnly key.Binding 19 | Enter key.Binding 20 | Edit key.Binding 21 | Find key.Binding 22 | Send key.Binding 23 | Command key.Binding 24 | Escape key.Binding 25 | ToggleBox key.Binding 26 | } 27 | 28 | // DefaultKeyMap returns a set of default keybindings. 29 | var Keys = KeyMap{ 30 | Quit: key.NewBinding( 31 | key.WithKeys("q", "ctrl+q"), 32 | ), 33 | Down: key.NewBinding( 34 | key.WithKeys("down"), 35 | ), 36 | Up: key.NewBinding( 37 | key.WithKeys("up"), 38 | ), 39 | Left: key.NewBinding( 40 | key.WithKeys("left"), 41 | ), 42 | Right: key.NewBinding( 43 | key.WithKeys("right"), 44 | ), 45 | Enter: key.NewBinding( 46 | key.WithKeys("enter"), 47 | ), 48 | Escape: key.NewBinding( 49 | key.WithKeys("esc"), 50 | ), 51 | View: key.NewBinding( 52 | key.WithKeys("V"), 53 | ), 54 | GotoBottom: key.NewBinding( 55 | key.WithKeys("G"), 56 | ), 57 | HomeShortcut: key.NewBinding( 58 | key.WithKeys("~"), 59 | ), 60 | RootShortcut: key.NewBinding( 61 | key.WithKeys("/"), 62 | ), 63 | ToggleHidden: key.NewBinding( 64 | key.WithKeys("."), 65 | ), 66 | ShowDirectoriesOnly: key.NewBinding( 67 | key.WithKeys("D"), 68 | ), 69 | ShowFilesOnly: key.NewBinding( 70 | key.WithKeys("F"), 71 | ), 72 | Edit: key.NewBinding( 73 | key.WithKeys("E"), 74 | ), 75 | Find: key.NewBinding( 76 | key.WithKeys("ctrl+f"), 77 | ), 78 | ToggleBox: key.NewBinding( 79 | key.WithKeys("tab"), 80 | ), 81 | Receive: key.NewBinding( 82 | key.WithKeys("ctrl+r"), 83 | ), 84 | Send: key.NewBinding( 85 | key.WithKeys("ctrl+s"), 86 | ), 87 | } 88 | -------------------------------------------------------------------------------- /internal/tui/model.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "image" 5 | "io/fs" 6 | 7 | "github.com/abdfnx/tran/constants" 8 | "github.com/charmbracelet/lipgloss" 9 | "github.com/abdfnx/tran/internal/theme" 10 | tea "github.com/charmbracelet/bubbletea" 11 | "github.com/abdfnx/tran/internal/config" 12 | "github.com/charmbracelet/bubbles/spinner" 13 | "github.com/charmbracelet/bubbles/viewport" 14 | "github.com/charmbracelet/bubbles/textinput" 15 | ) 16 | 17 | // Bubble represents the state of the UI. 18 | type Bubble struct { 19 | appConfig config.Config 20 | theme theme.Theme 21 | currentImage image.Image 22 | spinner spinner.Model 23 | textinput textinput.Model 24 | primaryViewport viewport.Model 25 | secondaryViewport viewport.Model 26 | thirdViewport viewport.Model 27 | treeFiles []fs.DirEntry 28 | treePreviewFiles []fs.DirEntry 29 | previousKey tea.KeyMsg 30 | keyMap KeyMap 31 | width int 32 | height int 33 | activeBox int 34 | treeCursor int 35 | showHiddenFiles bool 36 | ready bool 37 | showCommandInput bool 38 | showFilesOnly bool 39 | showDirectoriesOnly bool 40 | showFileTreePreview bool 41 | findMode bool 42 | sendMode bool 43 | receiveMode bool 44 | showBoxSpinner bool 45 | foundFilesPaths []string 46 | fileSizes []string 47 | secondaryBoxContent string 48 | errorMsg string 49 | } 50 | 51 | // New creates an instance of the entire application. 52 | func New() Bubble { 53 | cfg := config.GetConfig() 54 | theme := theme.GetTheme("default") 55 | 56 | primaryBoxBorder := lipgloss.RoundedBorder() 57 | secondaryBoxBorder := lipgloss.RoundedBorder() 58 | thirdBoxBorder := lipgloss.RoundedBorder() 59 | primaryBoxBorderColor := theme.ActiveBoxBorderColor 60 | secondaryBoxBorderColor := theme.InactiveBoxBorderColor 61 | thirdBoxBorderColor := theme.InactiveBoxBorderColor 62 | 63 | if cfg.Tran.Borderless { 64 | primaryBoxBorder = lipgloss.HiddenBorder() 65 | secondaryBoxBorder = lipgloss.HiddenBorder() 66 | thirdBoxBorder = lipgloss.HiddenBorder() 67 | } 68 | 69 | pvp := viewport.New(0, 0) 70 | pvp.Style = lipgloss.NewStyle(). 71 | PaddingLeft(constants.BoxPadding). 72 | PaddingRight(constants.BoxPadding). 73 | Border(primaryBoxBorder). 74 | BorderForeground(primaryBoxBorderColor) 75 | 76 | svp := viewport.New(0, 0) 77 | svp.Style = lipgloss.NewStyle(). 78 | PaddingLeft(constants.BoxPadding). 79 | PaddingRight(constants.BoxPadding). 80 | Border(secondaryBoxBorder). 81 | BorderForeground(secondaryBoxBorderColor) 82 | 83 | tvp := viewport.New(0, 0) 84 | tvp.Style = lipgloss.NewStyle(). 85 | PaddingLeft(constants.BoxPadding). 86 | PaddingRight(constants.BoxPadding). 87 | Border(thirdBoxBorder). 88 | BorderForeground(thirdBoxBorderColor) 89 | 90 | s := spinner.New() 91 | s.Spinner = spinner.Dot 92 | s.Style = lipgloss.NewStyle().Foreground(theme.SpinnerColor) 93 | 94 | t := textinput.New() 95 | t.Prompt = "❯ " 96 | t.CharLimit = 250 97 | t.PlaceholderStyle = lipgloss.NewStyle(). 98 | Background(theme.StatusBarBarBackgroundColor). 99 | Foreground(theme.StatusBarBarForegroundColor) 100 | 101 | return Bubble{ 102 | appConfig: cfg, 103 | theme: theme, 104 | showHiddenFiles: true, 105 | spinner: s, 106 | textinput: t, 107 | primaryViewport: pvp, 108 | secondaryViewport: svp, 109 | thirdViewport: tvp, 110 | keyMap: Keys, 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /internal/tui/receive.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "os" 5 | "fmt" 6 | "math" 7 | "time" 8 | 9 | "github.com/gorilla/websocket" 10 | "github.com/abdfnx/tran/tools" 11 | "github.com/abdfnx/tran/models" 12 | "github.com/abdfnx/tran/constants" 13 | "github.com/abdfnx/tran/core/receiver" 14 | "github.com/abdfnx/tran/models/protocol" 15 | tea "github.com/charmbracelet/bubbletea" 16 | ) 17 | 18 | // HandleReceiveCommand is the receive application. 19 | func HandleReceiveCommand(programOptions models.TranOptions, password string) { 20 | // communicate ui updates on this channel between receiverClient and handleReceiveCmmand 21 | uiCh := make(chan receiver.UIUpdate) 22 | // initialize a receiverClient with a UI 23 | receiverClient := receiver.WithUI(receiver.NewReceiver(programOptions), uiCh) 24 | // initialize and start receiver-UI 25 | receiverUI := NewReceiverUI() 26 | // clean up temporary files previously created by this command 27 | tools.RemoveTemporaryFiles(constants.RECEIVE_TEMP_FILE_NAME_PREFIX) 28 | 29 | go initReceiverUI(receiverUI) 30 | time.Sleep(constants.START_PERIOD) 31 | go listenForReceiverUIUpdates(receiverUI, uiCh) 32 | 33 | parsedPassword, err := tools.ParsePassword(password) 34 | if err != nil { 35 | receiverUI.Send(ErrorMsg{Message: "Error parsing password, make sure you entered a correctly formatted password (e.g. 1-gamma-ray-quasar)."}) 36 | GracefulUIQuit() 37 | } 38 | 39 | // initiate communications with tranx-server 40 | wsConnCh := make(chan *websocket.Conn) 41 | go initiateReceiverTranxCommunication(receiverClient, receiverUI, parsedPassword, wsConnCh) 42 | 43 | // keeps program alive until finished 44 | doneCh := make(chan bool) 45 | // start receiving files 46 | go startReceiving(receiverClient, receiverUI, <-wsConnCh, doneCh) 47 | 48 | // wait for shut down to render final UI 49 | <-doneCh 50 | GracefulUIQuit() 51 | } 52 | 53 | func initReceiverUI(receiverUI *tea.Program) { 54 | go func() { 55 | if err := receiverUI.Start(); err != nil { 56 | fmt.Println("Error initializing UI", err) 57 | os.Exit(1) 58 | } 59 | 60 | os.Exit(0) 61 | }() 62 | } 63 | 64 | func listenForReceiverUIUpdates(receiverUI *tea.Program, uiCh chan receiver.UIUpdate) { 65 | latestProgress := 0 66 | 67 | for uiUpdate := range uiCh { 68 | // limit progress update ui-send events 69 | newProgress := int(math.Ceil(100 * float64(uiUpdate.Progress))) 70 | if newProgress > latestProgress { 71 | latestProgress = newProgress 72 | receiverUI.Send(ProgressMsg{Progress: uiUpdate.Progress}) 73 | } 74 | } 75 | } 76 | 77 | func initiateReceiverTranxCommunication(receiverClient *receiver.Receiver, receiverUI *tea.Program, password models.Password, connectionCh chan *websocket.Conn) { 78 | wsConn, err := receiverClient.ConnectToTranx(receiverClient.TranxAddress(), receiverClient.TranxPort(), password) 79 | if err != nil { 80 | receiverUI.Send(ErrorMsg{Message: "Something went wrong during connection-negotiation (did you enter the correct password?)"}) 81 | GracefulUIQuit() 82 | } 83 | 84 | receiverUI.Send(FileInfoMsg{Bytes: receiverClient.PayloadSize()}) 85 | connectionCh <- wsConn 86 | } 87 | 88 | func startReceiving(receiverClient *receiver.Receiver, receiverUI *tea.Program, wsConnection *websocket.Conn, doneCh chan bool) { 89 | tempFile, err := os.CreateTemp(os.TempDir(), constants.RECEIVE_TEMP_FILE_NAME_PREFIX) 90 | 91 | if err != nil { 92 | receiverUI.Send(ErrorMsg{Message: "Something went wrong when creating the received file container."}) 93 | GracefulUIQuit() 94 | } 95 | 96 | defer os.Remove(tempFile.Name()) 97 | defer tempFile.Close() 98 | 99 | // start receiving files from sender 100 | err = receiverClient.Receive(wsConnection, tempFile) 101 | if err != nil { 102 | receiverUI.Send(ErrorMsg{Message: "Something went wrong during file transfer."}) 103 | GracefulUIQuit() 104 | } 105 | 106 | if receiverClient.UsedRelay() { 107 | wsConnection.WriteJSON(protocol.TranxMessage{Type: protocol.ReceiverToTranxClose}) 108 | } 109 | 110 | // reset file position for reading 111 | tempFile.Seek(0, 0) 112 | 113 | // read received bytes from tmpFile 114 | receivedFileNames, decompressedSize, err := tools.DecompressAndUnarchiveBytes(tempFile) 115 | if err != nil { 116 | receiverUI.Send(ErrorMsg{Message: "Something went wrong when expanding the received files."}) 117 | GracefulUIQuit() 118 | } 119 | 120 | receiverUI.Send(FinishedMsg{Files: receivedFileNames, PayloadSize: decompressedSize}) 121 | doneCh <- true 122 | } 123 | -------------------------------------------------------------------------------- /internal/tui/receiver.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/abdfnx/tran/tools" 8 | "github.com/muesli/reflow/indent" 9 | "github.com/abdfnx/tran/constants" 10 | "github.com/muesli/reflow/wordwrap" 11 | "github.com/charmbracelet/lipgloss" 12 | tea "github.com/charmbracelet/bubbletea" 13 | "github.com/charmbracelet/bubbles/spinner" 14 | "github.com/charmbracelet/bubbles/progress" 15 | ) 16 | 17 | type uiState int 18 | 19 | // ui state flows from the top down 20 | const ( 21 | showEstablishing uiState = iota 22 | showReceivingProgress 23 | showFinished 24 | showError 25 | ) 26 | 27 | type receiverUIModel struct { 28 | state uiState 29 | receivedFiles []string 30 | payloadSize int64 31 | decompressedPayloadSize int64 32 | spinner spinner.Model 33 | progressBar progress.Model 34 | errorMessage string 35 | } 36 | 37 | func NewReceiverUI() *tea.Program { 38 | m := receiverUIModel{ 39 | progressBar: constants.ProgressBar, 40 | } 41 | 42 | m.resetSpinner() 43 | var opts []tea.ProgramOption 44 | 45 | opts = append(opts, tea.WithAltScreen()) 46 | 47 | return tea.NewProgram(m, opts...) 48 | } 49 | 50 | func (receiverUIModel) Init() tea.Cmd { 51 | return spinner.Tick 52 | } 53 | 54 | func (m receiverUIModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 55 | switch msg := msg.(type) { 56 | case FileInfoMsg: 57 | m.payloadSize = msg.Bytes 58 | if m.state != showReceivingProgress { 59 | m.state = showReceivingProgress 60 | m.resetSpinner() 61 | 62 | return m, spinner.Tick 63 | } 64 | 65 | return m, nil 66 | 67 | case ProgressMsg: 68 | m.state = showReceivingProgress 69 | cmd := m.progressBar.SetPercent(float64(msg.Progress)) 70 | 71 | return m, cmd 72 | 73 | case FinishedMsg: 74 | m.state = showFinished 75 | m.receivedFiles = msg.Files 76 | m.decompressedPayloadSize = msg.PayloadSize 77 | cmd := m.progressBar.SetPercent(1.0) 78 | 79 | return m, cmd 80 | 81 | case ErrorMsg: 82 | m.state = showError 83 | m.errorMessage = msg.Message 84 | 85 | return m, nil 86 | 87 | case tea.KeyMsg: 88 | if tools.Contains(constants.QuitKeys, strings.ToLower(msg.String())) { 89 | return m, tea.Quit 90 | } 91 | 92 | return m, nil 93 | 94 | case tea.WindowSizeMsg: 95 | m.progressBar.Width = msg.Width - 2 * constants.PADDING - 4 96 | if m.progressBar.Width > constants.MAX_WIDTH { 97 | m.progressBar.Width = constants.MAX_WIDTH 98 | } 99 | 100 | return m, nil 101 | 102 | // FrameMsg is sent when the progress bar wants to animate itself 103 | case progress.FrameMsg: 104 | progressModel, cmd := m.progressBar.Update(msg) 105 | m.progressBar = progressModel.(progress.Model) 106 | 107 | return m, cmd 108 | 109 | default: 110 | var cmd tea.Cmd 111 | m.spinner, cmd = m.spinner.Update(msg) 112 | 113 | return m, cmd 114 | } 115 | } 116 | 117 | func (m receiverUIModel) View() string { 118 | switch m.state { 119 | case showEstablishing: 120 | return "\n" + 121 | constants.PadText + constants.InfoStyle(fmt.Sprintf("%s Establishing connection with sender", m.spinner.View())) + "\n\n" 122 | 123 | case showReceivingProgress: 124 | payloadSize := constants.BoldText(tools.ByteCountSI(m.payloadSize)) 125 | receivingText := fmt.Sprintf("%s Receiving files (total size %s)", m.spinner.View(), payloadSize) 126 | 127 | return "\n" + 128 | constants.PadText + constants.InfoStyle(receivingText) + "\n\n" + 129 | constants.PadText + m.progressBar.View() + "\n\n" + 130 | constants.PadText + constants.QuitCommandsHelpText + "\n\n" 131 | 132 | case showFinished: 133 | payloadSize := constants.BoldText(tools.ByteCountSI(m.payloadSize)) 134 | indentedWrappedFiles := indent.String(fmt.Sprintf("Received: %s", wordwrap.String(constants.ItalicText(TopLevelFilesText(m.receivedFiles)), constants.MAX_WIDTH)), constants.PADDING) 135 | finishedText := fmt.Sprintf("Received %d files (%s decompressed)\n\n%s", len(m.receivedFiles), payloadSize, indentedWrappedFiles) 136 | 137 | return "\n" + 138 | constants.PadText + constants.InfoStyle(finishedText) + "\n\n" + 139 | constants.PadText + m.progressBar.View() + "\n\n" + 140 | constants.PadText + constants.QuitCommandsHelpText + "\n\n" 141 | 142 | case showError: 143 | return m.errorMessage 144 | 145 | default: 146 | return "" 147 | } 148 | } 149 | 150 | func (m *receiverUIModel) resetSpinner() { 151 | m.spinner = spinner.NewModel() 152 | m.spinner.Style = lipgloss.NewStyle().Foreground(lipgloss.Color(constants.PRIMARY_COLOR)) 153 | 154 | if m.state == showEstablishing { 155 | m.spinner.Spinner = WaitingSpinner 156 | } 157 | 158 | if m.state == showReceivingProgress { 159 | m.spinner.Spinner = TransferSpinner 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /internal/tui/send.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "os" 5 | "fmt" 6 | "net" 7 | "math" 8 | "time" 9 | "errors" 10 | 11 | "github.com/gorilla/websocket" 12 | "github.com/abdfnx/tran/tools" 13 | "github.com/abdfnx/tran/models" 14 | "github.com/abdfnx/tran/constants" 15 | "github.com/abdfnx/tran/core/sender" 16 | tea "github.com/charmbracelet/bubbletea" 17 | ) 18 | 19 | func HandleSendCommand(programOptions models.TranOptions, fileNames []string) { 20 | // communicate ui updates on this channel between senderClient and HandleSendCommand 21 | uiCh := make(chan sender.UIUpdate) 22 | // initialize a senderClient with a UI 23 | senderClient := sender.WithUI(sender.NewSender(programOptions), uiCh) 24 | // initialize and start sender-UI 25 | senderUI := NewSenderUI() 26 | // clean up temporary files previously created by this command 27 | tools.RemoveTemporaryFiles(constants.SEND_TEMP_FILE_NAME_PREFIX) 28 | 29 | go initSenderUI(senderUI) 30 | time.Sleep(constants.START_PERIOD) 31 | go listenForSenderUIUpdates(senderUI, uiCh) 32 | 33 | closeFileCh := make(chan *os.File) 34 | senderReadyCh := make(chan bool, 1) 35 | // read, archive and compress files in parallel 36 | go prepareFiles(senderClient, senderUI, fileNames, senderReadyCh, closeFileCh) 37 | 38 | // initiate communications with tranx-server 39 | startServerCh := make(chan sender.ServerOptions) 40 | relayCh := make(chan *websocket.Conn) 41 | passCh := make(chan models.Password) 42 | go initiateSenderTranxCommunication(senderClient, senderUI, passCh, startServerCh, senderReadyCh, relayCh) 43 | senderUI.Send(PasswordMsg{Password: string(<-passCh)}) 44 | 45 | // keeps program alive until finished 46 | doneCh := make(chan bool) 47 | // attach server to senderClient 48 | senderClient = sender.WithServer(senderClient, <-startServerCh) 49 | 50 | go startDirectCommunicationServer(senderClient, senderUI, doneCh) 51 | // prepare a fallback to relay communications through tranx if direct communications unavailble 52 | prepareRelayCommunicationFallback(senderClient, senderUI, relayCh, doneCh) 53 | 54 | <-doneCh 55 | senderUI.Send(FinishedMsg{}) 56 | tempFile := <-closeFileCh 57 | os.Remove(tempFile.Name()) 58 | tempFile.Close() 59 | GracefulUIQuit() 60 | } 61 | 62 | func initSenderUI(senderUI *tea.Program) { 63 | if err := senderUI.Start(); err != nil { 64 | fmt.Println("Error initializing UI", err) 65 | os.Exit(1) 66 | } 67 | 68 | os.Exit(0) 69 | } 70 | 71 | func listenForSenderUIUpdates(senderUI *tea.Program, uiCh chan sender.UIUpdate) { 72 | latestProgress := 0 73 | for uiUpdate := range uiCh { 74 | // make sure progress is 100 if connection is to be closed 75 | if uiUpdate.State == sender.WaitForCloseMessage { 76 | latestProgress = 100 77 | senderUI.Send(ProgressMsg{Progress: 1}) 78 | continue 79 | } 80 | 81 | // limit progress update ui-send events 82 | newProgress := int(math.Ceil(100 * float64(uiUpdate.Progress))) 83 | if newProgress > latestProgress { 84 | latestProgress = newProgress 85 | senderUI.Send(ProgressMsg{Progress: uiUpdate.Progress}) 86 | } 87 | } 88 | } 89 | 90 | func prepareFiles(senderClient *sender.Sender, senderUI *tea.Program, fileNames []string, readyCh chan bool, closeFileCh chan *os.File) { 91 | files, err := tools.ReadFiles(fileNames) 92 | 93 | if err != nil { 94 | senderUI.Send(ErrorMsg{Message: "Error reading files."}) 95 | GracefulUIQuit() 96 | } 97 | 98 | uncompressedFileSize, err := tools.FilesTotalSize(files) 99 | if err != nil { 100 | senderUI.Send(ErrorMsg{Message: "Error during file preparation."}) 101 | GracefulUIQuit() 102 | } 103 | 104 | senderUI.Send(FileInfoMsg{FileNames: fileNames, Bytes: uncompressedFileSize}) 105 | 106 | tempFile, fileSize, err := tools.ArchiveAndCompressFiles(files) 107 | for _, file := range files { 108 | file.Close() 109 | } 110 | 111 | if err != nil { 112 | senderUI.Send(ErrorMsg{Message: "Error compressing files."}) 113 | GracefulUIQuit() 114 | } 115 | 116 | sender.WithPayload(senderClient, tempFile, fileSize) 117 | senderUI.Send(FileInfoMsg{FileNames: fileNames, Bytes: fileSize}) 118 | readyCh <- true 119 | senderUI.Send(ReadyMsg{}) 120 | closeFileCh <- tempFile 121 | } 122 | 123 | func initiateSenderTranxCommunication(senderClient *sender.Sender, senderUI *tea.Program, passCh chan models.Password, 124 | startServerCh chan sender.ServerOptions, readyCh chan bool, relayCh chan *websocket.Conn) { 125 | err := senderClient.ConnectToTranx( 126 | senderClient.TranxAddress(), senderClient.TranxPort(), passCh, startServerCh, readyCh, relayCh) 127 | 128 | if err != nil { 129 | senderUI.Send(ErrorMsg{Message: "Failed to communicate with tranx server."}) 130 | GracefulUIQuit() 131 | } 132 | } 133 | 134 | func startDirectCommunicationServer(senderClient *sender.Sender, senderUI *tea.Program, doneCh chan bool) { 135 | if err := senderClient.StartServer(); err != nil { 136 | senderUI.Send(ErrorMsg{Message: fmt.Sprintf("Something went wrong during file transfer: %e", err)}) 137 | GracefulUIQuit() 138 | } 139 | 140 | doneCh <- true 141 | } 142 | 143 | func prepareRelayCommunicationFallback(senderClient *sender.Sender, senderUI *tea.Program, relayCh chan *websocket.Conn, doneCh chan bool) { 144 | if relayWsConn, closed := <-relayCh; closed { 145 | // start transferring to the tranx-relay 146 | go func() { 147 | if err := senderClient.Transfer(relayWsConn); err != nil { 148 | senderUI.Send(ErrorMsg{Message: fmt.Sprintf("Something went wrong during file transfer: %e", err)}) 149 | GracefulUIQuit() 150 | } 151 | 152 | doneCh <- true 153 | }() 154 | } 155 | } 156 | 157 | func ValidateTranxAddress() error { 158 | address := net.ParseIP(constants.DEFAULT_ADDRESS) 159 | err := tools.ValidateHostname(constants.DEFAULT_ADDRESS) 160 | 161 | // neither a valid IP nor a valid hostname was provided 162 | if (address == nil) && err != nil { 163 | return errors.New("invalid IP or hostname provided") 164 | } 165 | 166 | return nil 167 | } 168 | -------------------------------------------------------------------------------- /internal/tui/sender.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strings" 7 | 8 | "github.com/abdfnx/tran/constants" 9 | "github.com/abdfnx/tran/tools" 10 | "github.com/charmbracelet/bubbles/progress" 11 | "github.com/charmbracelet/bubbles/spinner" 12 | tea "github.com/charmbracelet/bubbletea" 13 | "github.com/charmbracelet/lipgloss" 14 | "github.com/muesli/reflow/indent" 15 | "github.com/muesli/reflow/wordwrap" 16 | "github.com/muesli/termenv" 17 | ) 18 | 19 | // ui state flows from the top down 20 | const ( 21 | showPasswordWithCopy uiState = iota 22 | showPassword 23 | showSendingProgress 24 | showSFinished 25 | showSError 26 | ) 27 | 28 | type senderUIModel struct { 29 | state uiState 30 | fileNames []string 31 | payloadSize int64 32 | password string 33 | readyToSend bool 34 | spinner spinner.Model 35 | progressBar progress.Model 36 | errorMessage string 37 | } 38 | 39 | type ReadyMsg struct{} 40 | 41 | type PasswordMsg struct { 42 | Password string 43 | } 44 | 45 | func NewSenderUI() *tea.Program { 46 | m := senderUIModel{progressBar: constants.ProgressBar} 47 | m.resetSpinner() 48 | var opts []tea.ProgramOption 49 | 50 | termenv.AltScreen() 51 | 52 | opts = append(opts, tea.WithAltScreen()) 53 | 54 | return tea.NewProgram(m, opts...) 55 | } 56 | 57 | func (senderUIModel) Init() tea.Cmd { 58 | return spinner.Tick 59 | } 60 | 61 | func (m senderUIModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 62 | switch msg := msg.(type) { 63 | case FileInfoMsg: 64 | m.fileNames = msg.FileNames 65 | m.payloadSize = msg.Bytes 66 | 67 | return m, nil 68 | 69 | case ReadyMsg: 70 | m.readyToSend = true 71 | m.resetSpinner() 72 | 73 | return m, spinner.Tick 74 | 75 | case PasswordMsg: 76 | m.password = msg.Password 77 | 78 | return m, nil 79 | 80 | case ProgressMsg: 81 | if m.state != showSendingProgress { 82 | m.state = showSendingProgress 83 | m.resetSpinner() 84 | return m, spinner.Tick 85 | } 86 | 87 | if m.progressBar.Percent() == 1.0 { 88 | return m, nil 89 | } 90 | 91 | cmd := m.progressBar.SetPercent(float64(msg.Progress)) 92 | 93 | return m, cmd 94 | 95 | case FinishedMsg: 96 | m.state = showSFinished 97 | cmd := m.progressBar.SetPercent(1.0) 98 | 99 | return m, cmd 100 | 101 | case ErrorMsg: 102 | m.state = showSError 103 | m.errorMessage = msg.Message 104 | 105 | return m, nil 106 | 107 | case tea.KeyMsg: 108 | if tools.Contains(constants.QuitKeys, strings.ToLower(msg.String())) { 109 | return m, tea.Quit 110 | } 111 | 112 | return m, nil 113 | 114 | case tea.WindowSizeMsg: 115 | m.progressBar.Width = msg.Width - 2 * constants.PADDING - 4 116 | 117 | if m.progressBar.Width > constants.MAX_WIDTH { 118 | m.progressBar.Width = constants.MAX_WIDTH 119 | } 120 | 121 | return m, nil 122 | 123 | // FrameMsg is sent when the progress bar wants to animate itself 124 | case progress.FrameMsg: 125 | progressModel, cmd := m.progressBar.Update(msg) 126 | m.progressBar = progressModel.(progress.Model) 127 | 128 | return m, cmd 129 | 130 | default: 131 | var cmd tea.Cmd 132 | m.spinner, cmd = m.spinner.Update(msg) 133 | 134 | return m, cmd 135 | } 136 | } 137 | 138 | func (m senderUIModel) View() string { 139 | readiness := fmt.Sprintf("%s Compressing objects, preparing to send", m.spinner.View()) 140 | 141 | if m.readyToSend { 142 | readiness = fmt.Sprintf("%s Awaiting receiver, ready to send", m.spinner.View()) 143 | } 144 | 145 | if m.state == showSendingProgress { 146 | readiness = fmt.Sprintf("%s Sending", m.spinner.View()) 147 | } 148 | 149 | fileInfoText := fmt.Sprintf("%s object(s)...", readiness) 150 | 151 | if m.fileNames != nil && m.payloadSize != 0 { 152 | sort.Strings(m.fileNames) 153 | filesToSend := constants.ItalicText(strings.Join(m.fileNames, ", ")) 154 | payloadSize := constants.BoldText(tools.ByteCountSI(m.payloadSize)) 155 | fileInfoText = fmt.Sprintf("%s %d objects (%s)", readiness, len(m.fileNames), payloadSize) 156 | 157 | indentedWrappedFiles := indent.String(wordwrap.String(fmt.Sprintf("Sending: %s", filesToSend), constants.MAX_WIDTH), constants.PADDING) 158 | fileInfoText = fmt.Sprintf("%s\n\n%s", fileInfoText, indentedWrappedFiles) 159 | } 160 | 161 | switch m.state { 162 | case showPassword, showPasswordWithCopy: 163 | return "\n" + 164 | constants.PadText + constants.InfoStyle(fileInfoText) + "\n\n" + 165 | constants.PadText + "On the other computer, press " + constants.HelpStyle("`ctrl+r`") + " to enable receive mode and then enter the password:" + "\n\n" + 166 | constants.PadText + "This is the password: " + constants.BoldText(m.password) + "\n\n" 167 | 168 | case showSendingProgress: 169 | return "\n" + 170 | constants.PadText + constants.InfoStyle(fileInfoText) + "\n\n" + 171 | constants.PadText + m.progressBar.View() + "\n\n" + 172 | constants.PadText + constants.QuitCommandsHelpText + "\n\n" 173 | 174 | case showSFinished: 175 | payloadSize := constants.BoldText(tools.ByteCountSI(m.payloadSize)) 176 | indentedWrappedFiles := indent.String(fmt.Sprintf("Sent: %s", wordwrap.String(constants.ItalicText(TopLevelFilesText(m.fileNames)), constants.MAX_WIDTH)), constants.PADDING) 177 | finishedText := fmt.Sprintf("Sent %d objects (%s decompressed)\n\n%s", len(m.fileNames), payloadSize, indentedWrappedFiles) 178 | 179 | return "\n" + 180 | constants.PadText + constants.InfoStyle(finishedText) + "\n\n" + 181 | constants.PadText + m.progressBar.View() + "\n\n" + 182 | constants.PadText + constants.QuitCommandsHelpText + "\n\n" 183 | 184 | case showSError: 185 | return m.errorMessage 186 | 187 | default: 188 | return "" 189 | } 190 | } 191 | 192 | func (m *senderUIModel) resetSpinner() { 193 | m.spinner = spinner.NewModel() 194 | m.spinner.Style = lipgloss.NewStyle().Foreground(lipgloss.Color(constants.PRIMARY_COLOR)) 195 | 196 | if m.readyToSend { 197 | m.spinner.Spinner = WaitingSpinner 198 | } else { 199 | m.spinner.Spinner = CompressingSpinner 200 | } 201 | 202 | if m.state == showSendingProgress { 203 | m.spinner.Spinner = TransferSpinner 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /internal/tui/sr-ui.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "time" 7 | "strings" 8 | 9 | "github.com/abdfnx/tran/constants" 10 | "github.com/charmbracelet/bubbles/spinner" 11 | ) 12 | 13 | type UIUpdate struct { 14 | Progress float32 15 | } 16 | 17 | type FileInfoMsg struct { 18 | FileNames []string 19 | Bytes int64 20 | } 21 | 22 | type ErrorMsg struct { 23 | Message string 24 | } 25 | 26 | type ProgressMsg struct { 27 | Progress float32 28 | } 29 | 30 | type FinishedMsg struct { 31 | Files []string 32 | PayloadSize int64 33 | } 34 | 35 | var WaitingSpinner = spinner.Dot 36 | 37 | var CompressingSpinner = spinner.Globe 38 | 39 | var TransferSpinner = spinner.Spinner{ 40 | Frames: []string{"» ", "»» ", "»»»", " "}, 41 | FPS: time.Millisecond * 400, 42 | } 43 | 44 | var ReceivingSpinner = spinner.Spinner{ 45 | Frames: []string{" ", " «", " ««", "«««"}, 46 | FPS: time.Second / 2, 47 | } 48 | 49 | func TopLevelFilesText(fileNames []string) string { 50 | // parse top level file names and attach number of subfiles in them 51 | topLevelFileChildren := make(map[string]int) 52 | 53 | for _, f := range fileNames { 54 | fileTopPath := strings.Split(f, "/")[0] 55 | 56 | subfileCount, wasPresent := topLevelFileChildren[fileTopPath] 57 | 58 | if wasPresent { 59 | topLevelFileChildren[fileTopPath] = subfileCount + 1 60 | } else { 61 | topLevelFileChildren[fileTopPath] = 0 62 | } 63 | } 64 | 65 | // read map into formatted strings 66 | var topLevelFilesText []string 67 | 68 | for fileName, subFileCount := range topLevelFileChildren { 69 | formattedFileName := fileName 70 | 71 | if subFileCount > 0 { 72 | formattedFileName = fmt.Sprintf("%s (%d subfiles)", fileName, subFileCount) 73 | } 74 | 75 | topLevelFilesText = append(topLevelFilesText, formattedFileName) 76 | } 77 | 78 | sort.Strings(topLevelFilesText) 79 | 80 | return strings.Join(topLevelFilesText, ", ") 81 | } 82 | 83 | func GracefulUIQuit() { 84 | time.Sleep(constants.SHUTDOWN_PERIOD) 85 | } 86 | -------------------------------------------------------------------------------- /ios/color.go: -------------------------------------------------------------------------------- 1 | package ios 2 | 3 | import ( 4 | "os" 5 | "fmt" 6 | "strings" 7 | "strconv" 8 | 9 | "github.com/mgutz/ansi" 10 | ) 11 | 12 | var ( 13 | magenta = ansi.ColorFunc("magenta") 14 | cyan = ansi.ColorFunc("cyan") 15 | red = ansi.ColorFunc("red") 16 | yellow = ansi.ColorFunc("yellow") 17 | blue = ansi.ColorFunc("blue") 18 | green = ansi.ColorFunc("green") 19 | gray = ansi.ColorFunc("black+h") 20 | bold = ansi.ColorFunc("default+b") 21 | cyanBold = ansi.ColorFunc("cyan+b") 22 | 23 | gray256 = func(t string) string { 24 | return fmt.Sprintf("\x1b[%d;5;%dm%s\x1b[m", 38, 242, t) 25 | } 26 | ) 27 | 28 | func EnvColorDisabled() bool { 29 | return os.Getenv("NO_COLOR") != "" || os.Getenv("CLICOLOR") == "0" 30 | } 31 | 32 | func EnvColorForced() bool { 33 | return os.Getenv("CLICOLOR_FORCE") != "" && os.Getenv("CLICOLOR_FORCE") != "0" 34 | } 35 | 36 | func Is256ColorSupported() bool { 37 | return IsTrueColorSupported() || 38 | strings.Contains(os.Getenv("TERM"), "256") || 39 | strings.Contains(os.Getenv("COLORTERM"), "256") 40 | } 41 | 42 | func IsTrueColorSupported() bool { 43 | term := os.Getenv("TERM") 44 | colorterm := os.Getenv("COLORTERM") 45 | 46 | return strings.Contains(term, "24bit") || 47 | strings.Contains(term, "truecolor") || 48 | strings.Contains(colorterm, "24bit") || 49 | strings.Contains(colorterm, "truecolor") 50 | } 51 | 52 | func NewColorScheme(enabled, is256enabled bool) *ColorScheme { 53 | return &ColorScheme{ 54 | enabled: enabled, 55 | is256enabled: is256enabled, 56 | } 57 | } 58 | 59 | type ColorScheme struct { 60 | enabled bool 61 | is256enabled bool 62 | hasTrueColor bool 63 | } 64 | 65 | func (c *ColorScheme) Bold(t string) string { 66 | if !c.enabled { 67 | return t 68 | } 69 | 70 | return bold(t) 71 | } 72 | 73 | func (c *ColorScheme) Boldf(t string, args ...interface{}) string { 74 | return c.Bold(fmt.Sprintf(t, args...)) 75 | } 76 | 77 | func (c *ColorScheme) Red(t string) string { 78 | if !c.enabled { 79 | return t 80 | } 81 | 82 | return red(t) 83 | } 84 | 85 | func (c *ColorScheme) Redf(t string, args ...interface{}) string { 86 | return c.Red(fmt.Sprintf(t, args...)) 87 | } 88 | 89 | func (c *ColorScheme) Yellow(t string) string { 90 | if !c.enabled { 91 | return t 92 | } 93 | 94 | return yellow(t) 95 | } 96 | 97 | func (c *ColorScheme) Yellowf(t string, args ...interface{}) string { 98 | return c.Yellow(fmt.Sprintf(t, args...)) 99 | } 100 | 101 | func (c *ColorScheme) Green(t string) string { 102 | if !c.enabled { 103 | return t 104 | } 105 | 106 | return green(t) 107 | } 108 | 109 | func (c *ColorScheme) Greenf(t string, args ...interface{}) string { 110 | return c.Green(fmt.Sprintf(t, args...)) 111 | } 112 | 113 | func (c *ColorScheme) Gray(t string) string { 114 | if !c.enabled { 115 | return t 116 | } 117 | 118 | if c.is256enabled { 119 | return gray256(t) 120 | } 121 | 122 | return gray(t) 123 | } 124 | 125 | func (c *ColorScheme) Grayf(t string, args ...interface{}) string { 126 | return c.Gray(fmt.Sprintf(t, args...)) 127 | } 128 | 129 | func (c *ColorScheme) Magenta(t string) string { 130 | if !c.enabled { 131 | return t 132 | } 133 | 134 | return magenta(t) 135 | } 136 | 137 | func (c *ColorScheme) Magentaf(t string, args ...interface{}) string { 138 | return c.Magenta(fmt.Sprintf(t, args...)) 139 | } 140 | 141 | func (c *ColorScheme) Cyan(t string) string { 142 | if !c.enabled { 143 | return t 144 | } 145 | 146 | return cyan(t) 147 | } 148 | 149 | func (c *ColorScheme) Cyanf(t string, args ...interface{}) string { 150 | return c.Cyan(fmt.Sprintf(t, args...)) 151 | } 152 | 153 | func (c *ColorScheme) CyanBold(t string) string { 154 | if !c.enabled { 155 | return t 156 | } 157 | 158 | return cyanBold(t) 159 | } 160 | 161 | func (c *ColorScheme) Blue(t string) string { 162 | if !c.enabled { 163 | return t 164 | } 165 | 166 | return blue(t) 167 | } 168 | 169 | func (c *ColorScheme) Bluef(t string, args ...interface{}) string { 170 | return c.Blue(fmt.Sprintf(t, args...)) 171 | } 172 | 173 | func (c *ColorScheme) SuccessIcon() string { 174 | return c.SuccessIconWithColor(c.Green) 175 | } 176 | 177 | func (c *ColorScheme) SuccessIconWithColor(colo func(string) string) string { 178 | return colo("✓") 179 | } 180 | 181 | func (c *ColorScheme) WarningIcon() string { 182 | return c.Yellow("!") 183 | } 184 | 185 | func (c *ColorScheme) FailureIcon() string { 186 | return c.FailureIconWithColor(c.Red) 187 | } 188 | 189 | func (c *ColorScheme) FailureIconWithColor(colo func(string) string) string { 190 | return colo("X") 191 | } 192 | 193 | func (c *ColorScheme) ColorFromString(s string) func(string) string { 194 | s = strings.ToLower(s) 195 | var fn func(string) string 196 | 197 | switch s { 198 | case "bold": 199 | fn = c.Bold 200 | case "red": 201 | fn = c.Red 202 | case "yellow": 203 | fn = c.Yellow 204 | case "green": 205 | fn = c.Green 206 | case "gray": 207 | fn = c.Gray 208 | case "magenta": 209 | fn = c.Magenta 210 | case "cyan": 211 | fn = c.Cyan 212 | case "blue": 213 | fn = c.Blue 214 | default: 215 | fn = func(s string) string { 216 | return s 217 | } 218 | } 219 | 220 | return fn 221 | } 222 | 223 | func (c *ColorScheme) HexToRGB(hex string, x string) string { 224 | if !c.enabled || !c.hasTrueColor { 225 | return x 226 | } 227 | 228 | r, _ := strconv.ParseInt(hex[0:2], 16, 64) 229 | g, _ := strconv.ParseInt(hex[2:4], 16, 64) 230 | b, _ := strconv.ParseInt(hex[4:6], 16, 64) 231 | 232 | return fmt.Sprintf("\033[38;2;%d;%d;%dm%s\033[0m", r, g, b, x) 233 | } 234 | -------------------------------------------------------------------------------- /ios/console.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package ios 4 | 5 | import ( 6 | "os" 7 | "errors" 8 | ) 9 | 10 | func (s *IOStreams) EnableVirtualTerminalProcessing() error { 11 | return nil 12 | } 13 | 14 | func enableVirtualTerminalProcessing(f *os.File) error { 15 | return errors.New("not implemented") 16 | } 17 | -------------------------------------------------------------------------------- /ios/console_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package ios 4 | 5 | import ( 6 | "os" 7 | 8 | "golang.org/x/sys/windows" 9 | ) 10 | 11 | func (s *IOStreams) EnableVirtualTerminalProcessing() error { 12 | if !s.IsStdoutTTY() { 13 | return nil 14 | } 15 | 16 | f, ok := s.originalOut.(*os.File) 17 | if !ok { 18 | return nil 19 | } 20 | 21 | return enableVirtualTerminalProcessing(f) 22 | } 23 | 24 | func enableVirtualTerminalProcessing(f *os.File) error { 25 | stdout := windows.Handle(f.Fd()) 26 | 27 | var originalMode uint32 28 | windows.GetConsoleMode(stdout, &originalMode) 29 | 30 | return windows.SetConsoleMode(stdout, originalMode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING) 31 | } 32 | -------------------------------------------------------------------------------- /ios/tty_size.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package ios 5 | 6 | import ( 7 | "os" 8 | 9 | "golang.org/x/term" 10 | ) 11 | 12 | // ttySize measures the size of the controlling terminal for the current process 13 | func ttySize() (int, int, error) { 14 | f, err := os.Open("/dev/tty") 15 | 16 | if err != nil { 17 | return -1, -1, err 18 | } 19 | 20 | defer f.Close() 21 | 22 | return term.GetSize(int(f.Fd())) 23 | } 24 | -------------------------------------------------------------------------------- /ios/tty_size_windows.go: -------------------------------------------------------------------------------- 1 | package ios 2 | 3 | import ( 4 | "os" 5 | 6 | "golang.org/x/term" 7 | ) 8 | 9 | func ttySize() (int, int, error) { 10 | f, err := os.Open("CONOUT$") 11 | 12 | if err != nil { 13 | return -1, -1, err 14 | } 15 | 16 | defer f.Close() 17 | 18 | return term.GetSize(int(f.Fd())) 19 | } 20 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "fmt" 6 | "errors" 7 | "runtime" 8 | 9 | "github.com/mgutz/ansi" 10 | "github.com/spf13/cobra" 11 | "github.com/abdfnx/tran/tools" 12 | "github.com/abdfnx/tran/cmd/tran" 13 | "github.com/abdfnx/tran/cmd/factory" 14 | "github.com/abdfnx/tran/app/checker" 15 | "github.com/AlecAivazis/survey/v2/terminal" 16 | surveyCore "github.com/AlecAivazis/survey/v2/core" 17 | ) 18 | 19 | var ( 20 | version string 21 | buildDate string 22 | ) 23 | 24 | type exitCode int 25 | 26 | const ( 27 | exitOK exitCode = 0 28 | exitError exitCode = 1 29 | exitCancel exitCode = 2 30 | ) 31 | 32 | func main() { 33 | code := mainRun() 34 | os.Exit(int(code)) 35 | } 36 | 37 | func mainRun() exitCode { 38 | runtime.LockOSThread() 39 | 40 | cmdFactory := factory.New() 41 | hasDebug := os.Getenv("DEBUG") != "" 42 | stderr := cmdFactory.IOStreams.ErrOut 43 | 44 | if !cmdFactory.IOStreams.ColorEnabled() { 45 | surveyCore.DisableColor = true 46 | } else { 47 | surveyCore.TemplateFuncsWithColor["color"] = func(style string) string { 48 | switch style { 49 | case "white": 50 | if cmdFactory.IOStreams.ColorSupport256() { 51 | return fmt.Sprintf("\x1b[%d;5;%dm", 38, 242) 52 | } 53 | 54 | return ansi.ColorCode("default") 55 | 56 | default: 57 | return ansi.ColorCode(style) 58 | } 59 | } 60 | } 61 | 62 | if len(os.Args) > 1 && os.Args[1] != "" { 63 | cobra.MousetrapHelpText = "" 64 | } 65 | 66 | RootCmd := tran.Execute(cmdFactory, version, buildDate) 67 | 68 | if cmd, err := RootCmd.ExecuteC(); err != nil { 69 | if err == tools.SilentError { 70 | return exitError 71 | } else if tools.IsUserCancellation(err) { 72 | if errors.Is(err, terminal.InterruptErr) { 73 | fmt.Fprint(stderr, "\n") 74 | } 75 | 76 | return exitCancel 77 | } 78 | 79 | tools.PrintError(stderr, err, cmd, hasDebug) 80 | 81 | return exitError 82 | } 83 | 84 | if tran.HasFailed() { 85 | return exitError 86 | } 87 | 88 | if len(os.Args) > 1 && os.Args[1] != "tran" { 89 | checker.Check(version) 90 | } 91 | 92 | return exitOK 93 | } 94 | -------------------------------------------------------------------------------- /models/model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/abdfnx/tran/ios" 4 | 5 | type TranOptions struct { 6 | TranxAddress string 7 | TranxPort int 8 | Auth AuthLogin 9 | } 10 | 11 | type AuthLogin struct { 12 | Token string 13 | Hostname string 14 | IO *ios.IOStreams 15 | } 16 | 17 | type Password string 18 | -------------------------------------------------------------------------------- /models/protocol/transfer.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strings" 7 | ) 8 | 9 | // TransferMessageType specifies the message type for the messages in the transfer protocol. 10 | type TransferMessageType int 11 | 12 | const ( 13 | TransferError TransferMessageType = iota // An error has occured in transferProtocol 14 | ReceiverHandshake // Receiver exchange its IP via the tranx server to the sender 15 | SenderHandshake // Sender exchanges IP, port and payload size to the receiver via the tranx server 16 | ReceiverDirectCommunication 17 | SenderDirectAck // Sender ACKs the request for direct communication 18 | ReceiverRelayCommunication // Receiver has tried to probe the sender but cannot find it on the subnet, relay communication will be used 19 | SenderRelayAck // Sender ACKs the request for relay communication 20 | ReceiverRequestPayload // Receiver request the payload from the sender 21 | SenderPayloadSent // Sender announces that the entire file has been transfered 22 | ReceiverPayloadAck // Receiver ACKs that is has received the payload 23 | SenderClosing // Sender announces that it is closing the connection 24 | ReceiverClosingAck // Receiver ACKs the closing of the connection 25 | ) 26 | 27 | // TransferMessage specifies a message in the transfer protocol. 28 | type TransferMessage struct { 29 | Type TransferMessageType `json:"type"` 30 | Payload interface{} `json:"payload,omitempty"` 31 | } 32 | 33 | func (t TransferMessage) Bytes() []byte { 34 | return []byte(fmt.Sprintf("%v", t)) 35 | } 36 | 37 | type ReceiverHandshakePayload struct { 38 | IP net.IP `json:"ip"` 39 | } 40 | 41 | // SenderHandshakePayload specifies a payload type for announcing the payload size. 42 | type SenderHandshakePayload struct { 43 | IP net.IP `json:"ip"` 44 | Port int `json:"port"` 45 | PayloadSize int64 `json:"payload_size"` 46 | } 47 | 48 | type WrongMessageTypeError struct { 49 | expected []TransferMessageType 50 | got TransferMessageType 51 | } 52 | 53 | func NewWrongMessageTypeError(expected []TransferMessageType, got TransferMessageType) *WrongMessageTypeError { 54 | return &WrongMessageTypeError{ 55 | expected: expected, 56 | got: got, 57 | } 58 | } 59 | 60 | func (e *WrongMessageTypeError) Error() string { 61 | var expectedMessageTypes []string 62 | 63 | for _, expectedType := range e.expected { 64 | expectedMessageTypes = append(expectedMessageTypes, expectedType.Name()) 65 | } 66 | 67 | oneOfExpected := strings.Join(expectedMessageTypes, ", ") 68 | 69 | return fmt.Sprintf("wrong message type, expected one of: (%s), got: (%s)", oneOfExpected, e.got.Name()) 70 | } 71 | 72 | func (t TransferMessageType) Name() string { 73 | switch t { 74 | case TransferError: 75 | return "TransferError" 76 | 77 | case ReceiverHandshake: 78 | return "ReceiverHandshake" 79 | 80 | case SenderHandshake: 81 | return "SenderHandshake" 82 | 83 | case ReceiverRelayCommunication: 84 | return "ReceiverRelayCommunication" 85 | 86 | case SenderRelayAck: 87 | return "SenderRelayAck" 88 | 89 | case ReceiverRequestPayload: 90 | return "ReceiverRequestPayload" 91 | 92 | case SenderPayloadSent: 93 | return "SenderPayloadSent" 94 | 95 | case ReceiverPayloadAck: 96 | return "ReceiverAckPayload" 97 | 98 | case SenderClosing: 99 | return "SenderClosing" 100 | 101 | case ReceiverClosingAck: 102 | return "ReceiverClosingAck" 103 | 104 | default: 105 | return "" 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /models/protocol/tranx.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/gorilla/websocket" 7 | ) 8 | 9 | type TranxMessageType int 10 | 11 | const ( 12 | TranxToSenderBind TranxMessageType = iota // An ID for this connection is bound and communicated 13 | SenderToTranxEstablish // Sender has generated and hashed password 14 | ReceiverToTranxEstablish // Passsword has been communicated to receiver who has hashed it 15 | TranxToSenderReady // Tranx announces to sender that receiver is connected 16 | SenderToTranxPAKE // Sender sends PAKE information to tranx 17 | TranxToReceiverPAKE // Tranx forwards PAKE information to receiver 18 | ReceiverToTranxPAKE // Receiver sends PAKE information to tranx 19 | TranxToSenderPAKE // Tranx forwards PAKE information to receiver 20 | SenderToTranxSalt // Sender sends cryptographic salt to tranx 21 | TranxToReceiverSalt // Rendevoux forwards cryptographic salt to receiver 22 | ReceiverToTranxClose // Receiver can connect directly to sender, close receiver connection -> close sender connection 23 | SenderToTranxClose // Transit sequence is completed, close sender connection -> close receiver connection 24 | ) 25 | 26 | type TranxMessage struct { 27 | Type TranxMessageType `json:"type"` 28 | Payload interface{} `json:"payload"` 29 | } 30 | 31 | type TranxClient struct { 32 | Conn *websocket.Conn 33 | IP net.IP 34 | } 35 | 36 | type TranxSender struct { 37 | TranxClient 38 | Port int 39 | } 40 | 41 | type TranxReceiver = TranxClient 42 | 43 | /* [💻 Receiver <-> Sender 💻] messages */ 44 | 45 | type PasswordPayload struct { 46 | Password string `json:"password"` 47 | } 48 | type PakePayload struct { 49 | Bytes []byte `json:"pake_bytes"` 50 | } 51 | 52 | type SaltPayload struct { 53 | Salt []byte `json:"salt"` 54 | } 55 | 56 | /* [Tranx -> Sender] messages */ 57 | 58 | type TranxToSenderBindPayload struct { 59 | ID int `json:"id"` 60 | } 61 | -------------------------------------------------------------------------------- /renderer/renderer.go: -------------------------------------------------------------------------------- 1 | package renderer 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "bytes" 7 | "strings" 8 | 9 | "github.com/ledongthuc/pdf" 10 | "github.com/charmbracelet/glamour" 11 | "github.com/charmbracelet/lipgloss" 12 | "github.com/disintegration/imaging" 13 | "github.com/lucasb-eyer/go-colorful" 14 | "github.com/alecthomas/chroma/quick" 15 | ) 16 | 17 | // ConvertByesToSizeString converts a byte count to a human readable string. 18 | func ConvertBytesToSizeString(size int64) string { 19 | if size < 1000 { 20 | return fmt.Sprintf("%dB", size) 21 | } 22 | 23 | suffix := []string{ 24 | "K", // kilo 25 | "M", // mega 26 | "G", // giga 27 | "T", // tera 28 | "P", // peta 29 | "E", // exa 30 | "Z", // zeta 31 | "Y", // yotta 32 | } 33 | 34 | curr := float64(size) / 1000 35 | for _, s := range suffix { 36 | if curr < 10 { 37 | return fmt.Sprintf("%.1f%s", curr-0.0499, s) 38 | } else if curr < 1000 { 39 | return fmt.Sprintf("%d%s", int(curr), s) 40 | } 41 | 42 | curr /= 1000 43 | } 44 | 45 | return "" 46 | } 47 | 48 | // ImageToString converts an image to a string representation of an image. 49 | func ImageToString(width int, img image.Image) string { 50 | img = imaging.Resize(img, width, 0, imaging.Lanczos) 51 | b := img.Bounds() 52 | w := b.Max.X 53 | h := b.Max.Y 54 | str := strings.Builder{} 55 | 56 | for y := 0; y < h; y += 2 { 57 | for x := w; x < width; x += 2 { 58 | str.WriteString(" ") 59 | } 60 | 61 | for x := 0; x < w; x++ { 62 | c1, _ := colorful.MakeColor(img.At(x, y)) 63 | color1 := lipgloss.Color(c1.Hex()) 64 | c2, _ := colorful.MakeColor(img.At(x, y+1)) 65 | color2 := lipgloss.Color(c2.Hex()) 66 | str.WriteString(lipgloss.NewStyle().Foreground(color1). 67 | Background(color2).Render("▀")) 68 | } 69 | 70 | str.WriteString("\n") 71 | } 72 | 73 | return str.String() 74 | } 75 | 76 | // RenderMarkdown renders the markdown content with glamour. 77 | func RenderMarkdown(width int, content string) (string, error) { 78 | bg := "light" 79 | 80 | if lipgloss.HasDarkBackground() { 81 | bg = "dark" 82 | } 83 | 84 | r, _ := glamour.NewTermRenderer( 85 | glamour.WithWordWrap(width), 86 | glamour.WithStandardStyle(bg), 87 | ) 88 | 89 | out, err := r.Render(content) 90 | if err != nil { 91 | return "", err 92 | } 93 | 94 | return out, nil 95 | } 96 | 97 | // ReadPdf reads a PDF file given a name. 98 | func ReadPdf(name string) (string, error) { 99 | f, r, err := pdf.Open(name) 100 | if err != nil { 101 | return "", err 102 | } 103 | 104 | defer f.Close() 105 | 106 | buf := new(bytes.Buffer) 107 | b, err := r.GetPlainText() 108 | 109 | if err != nil { 110 | return "", err 111 | } 112 | 113 | _, err = buf.ReadFrom(b) 114 | if err != nil { 115 | return "", err 116 | } 117 | 118 | return buf.String(), nil 119 | } 120 | 121 | // Highlight returns a syntax highlighted string of text. 122 | func Highlight(content, extension, syntaxTheme string) (string, error) { 123 | buf := new(bytes.Buffer) 124 | if err := quick.Highlight(buf, content, extension, "terminal256", syntaxTheme); err != nil { 125 | return "", err 126 | } 127 | 128 | return buf.String(), nil 129 | } 130 | -------------------------------------------------------------------------------- /schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "tran settings", 4 | "description": "tran settings\nhttps://github.com/abdfnx/tran?tab=readme-ov-file#tran-config-file", 5 | "type": "object", 6 | "properties": { 7 | "config": { 8 | "title": "config", 9 | "description": "tran settings\nhttps://github.com/abdfnx/tran?tab=readme-ov-file#tran-config-file", 10 | "type": "object", 11 | "properties": { 12 | "borderless": { 13 | "title": "borderless", 14 | "description": "Whether to disable borders or not\nhttps://github.com/abdfnx/tran?tab=readme-ov-file#tran-config-file", 15 | "type": "boolean", 16 | "default": true 17 | }, 18 | "editor": { 19 | "title": "editor", 20 | "description": "An editor\nhttps://github.com/abdfnx/tran?tab=readme-ov-file#tran-config-file", 21 | "type": "string", 22 | "minLength": 1, 23 | "pattern": "[^ ]", 24 | "default": "vim" 25 | }, 26 | "enable_mousewheel": { 27 | "title": "enable mouse wheel", 28 | "description": "Whether to enable a mouse wheel or not\nhttps://github.com/abdfnx/tran?tab=readme-ov-file#tran-config-file", 29 | "type": "boolean", 30 | "default": true 31 | }, 32 | "show_updates": { 33 | "title": "show updates", 34 | "description": "Whether to show updates or not\nhttps://github.com/abdfnx/tran?tab=readme-ov-file#tran-config-file", 35 | "type": "boolean", 36 | "default": true 37 | }, 38 | "start_dir": { 39 | "title": "starting directory", 40 | "description": "A starting directory\nhttps://github.com/abdfnx/tran?tab=readme-ov-file#tran-config-file", 41 | "type": "string", 42 | "minLength": 1, 43 | "pattern": "[^ ]", 44 | "default": "." 45 | } 46 | }, 47 | "minProperties": 1, 48 | "additionalProperties": false 49 | } 50 | }, 51 | "minProperties": 1, 52 | "additionalProperties": false 53 | } 54 | -------------------------------------------------------------------------------- /scripts/bfs.ps1: -------------------------------------------------------------------------------- 1 | # Build From Source 2 | $loc = "$HOME\AppData\Local\tran" 3 | 4 | go run scripts/date.go >> date.txt 5 | 6 | $LATEST_VERSION=git describe --abbrev=0 --tags 7 | $DATE=cat date.txt 8 | 9 | # Build 10 | go mod tidy 11 | go build -o tran.exe -ldflags "-X main.version=$LATEST_VERSION -X main.versionDate=$DATE" 12 | 13 | # Setup 14 | $BIN = "$loc\bin" 15 | New-Item -ItemType "directory" -Path $BIN 16 | Move-Item tran.exe -Destination $BIN 17 | [System.Environment]::SetEnvironmentVariable("Path", $Env:Path + ";$BIN", [System.EnvironmentVariableTarget]::User) 18 | 19 | if (Test-Path -path $loc) { 20 | Write-Host "Tran was built successfully, refresh your powershell and then run 'tran --help'" -ForegroundColor DarkGreen 21 | } else { 22 | Write-Host "Build failed" -ForegroundColor Red 23 | } 24 | -------------------------------------------------------------------------------- /scripts/date.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | func main() { 9 | currentTime := time.Now() 10 | 11 | fmt.Println("(" + currentTime.Format("2006-01-02") + ")") 12 | } 13 | -------------------------------------------------------------------------------- /scripts/gh-tran/gh-trn.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const rm = require("rimraf"); 4 | const mkdirp = require("mkdirp"); 5 | const sh = require("shelljs"); 6 | 7 | const VERSION_CMD = sh.exec("git describe --abbrev=0 --tags"); 8 | const VERSION_DATE_CMD = sh.exec("go run ./scripts/date.go"); 9 | 10 | const VERSION = VERSION_CMD.replace("\n", "").replace("\r", ""); 11 | const VERSION_DATE = VERSION_DATE_CMD.replace("\n", "").replace("\r", ""); 12 | 13 | const ROOT = __dirname; 14 | const TEMPLATES = path.join(ROOT, "templates"); 15 | 16 | async function updateTranExtension(ghTranDir) { 17 | const templatePath = path.join(TEMPLATES, "gh-tran"); 18 | const template = fs.readFileSync(templatePath).toString("utf-8"); 19 | 20 | const templateReplaced = template 21 | .replace("CLI_VERSION", VERSION) 22 | .replace("CLI_VERSION_DATE", VERSION_DATE); 23 | 24 | fs.writeFileSync(path.join(ghTranDir, "gh-tran"), templateReplaced); 25 | } 26 | 27 | async function updateExtension() { 28 | const tmp = path.join(__dirname, "tmp"); 29 | const extensionDir = path.join(tmp, "gh-tran"); 30 | 31 | mkdirp.sync(tmp); 32 | rm.sync(extensionDir); 33 | 34 | console.log(`cloning https://github.com/abdfnx/gh-tran to ${extensionDir}`); 35 | 36 | sh.exec(`git clone https://github.com/abdfnx/gh-tran.git ${extensionDir}`) 37 | 38 | console.log(`done cloning abdfnx/gh-tran to ${extensionDir}`); 39 | 40 | console.log("updating local git..."); 41 | 42 | await updateTranExtension(extensionDir); 43 | } 44 | 45 | updateExtension().catch((err) => { 46 | console.error(`error running scripts/gh-tran/gh-trn.js`, err); 47 | process.exit(1); 48 | }); 49 | -------------------------------------------------------------------------------- /scripts/gh-tran/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gh-trn-brew", 3 | "version": "0.0.0", 4 | "description": "gh cli extension of tran", 5 | "author": "@abdfnx", 6 | "main": "gh-trn.js", 7 | "scripts": { 8 | "start": "node gh-trn.js" 9 | }, 10 | "keywords": [ 11 | "tran", 12 | "github" 13 | ], 14 | "dependencies": { 15 | "mkdirp": "^3.0.1", 16 | "rimraf": "^6.0.1", 17 | "shelljs": "^0.8.5" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /scripts/gh-tran/templates/gh-tran: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | repo="abdfnx/gh-tran" 5 | tag="CLI_VERSION" 6 | buildDate="CLI_VERSION_DATE" 7 | 8 | extensionPath="$(dirname "$0")" 9 | arch="$(uname -m)" 10 | 11 | exe="" 12 | 13 | if uname -a | grep Msys > /dev/null; then 14 | if [ $arch = "x86_64" ]; then 15 | exe="windows-x86_64" 16 | elif [ $arch = "i686" ]; then 17 | exe="windows-i386" 18 | elif [ $arch = "i386" ]; then 19 | exe="windows-i386" 20 | fi 21 | elif uname -a | grep Darwin > /dev/null; then 22 | if [ $arch = "x86_64" ]; then 23 | exe="darwin-x86_64" 24 | fi 25 | elif uname -a | grep Linux > /dev/null; then 26 | if [ $arch = "x86_64" ]; then 27 | exe="linux-x86_64" 28 | elif [ $arch = "i686" ]; then 29 | exe="linux-i38" 30 | elif [ $arch = "i386" ]; then 31 | exe="linux-i386" 32 | fi 33 | fi 34 | 35 | if [ "${exe}" == "" ]; then 36 | if [ "$(which go)" = "" ]; then 37 | echo "go must be installed to use this gh extension on this platform" 38 | exit 1 39 | fi 40 | 41 | exe="cmd.out" 42 | 43 | cd "${extensionPath}" > /dev/null 44 | go build -o "${exe}" -ldflags "-X main.version=${tag} -X main.buildDate=${buildDate}" 45 | cd - > /dev/null 46 | else 47 | if [[ ! -x "${extensionPath}/bin/${exe}" ]]; then 48 | mkdir -p "${extensionPath}/bin" 49 | rm -f "${extensionPath}/bin/*" 50 | gh release -R"${repo}" download "${tag}" -p "${exe}" --dir="${extensionPath}/bin" 51 | chmod +x "${extensionPath}/bin/${exe}" 52 | fi 53 | fi 54 | 55 | exec "${extensionPath}/bin/${exe}" "$@" 56 | -------------------------------------------------------------------------------- /scripts/install.ps1: -------------------------------------------------------------------------------- 1 | # get latest release 2 | $release_url = "https://api.github.com/repos/abdfnx/tran/releases" 3 | $tag = (Invoke-WebRequest -Uri $release_url -UseBasicParsing | ConvertFrom-Json)[0].tag_name 4 | $loc = "$HOME\AppData\Local\tran" 5 | $url = "" 6 | $arch = $env:PROCESSOR_ARCHITECTURE 7 | $releases_api_url = "https://github.com/abdfnx/tran/releases/download/$tag/tran_windows_${tag}" 8 | 9 | if ($arch -eq "AMD64") { 10 | $url = "${releases_api_url}_amd64.zip" 11 | } elseif ($arch -eq "x86") { 12 | $url = "${releases_api_url}_386.zip" 13 | } elseif ($arch -eq "arm") { 14 | $url = "${releases_api_url}_arm.zip" 15 | } elseif ($arch -eq "arm64") { 16 | $url = "${releases_api_url}_arm64.zip" 17 | } 18 | 19 | if (Test-Path -path $loc) { 20 | Remove-Item $loc -Recurse -Force 21 | } 22 | 23 | Write-Host "Installing tran version $tag" -ForegroundColor DarkCyan 24 | 25 | Invoke-WebRequest $url -outfile tran_windows.zip 26 | 27 | Expand-Archive tran_windows.zip 28 | 29 | New-Item -ItemType "directory" -Path $loc 30 | 31 | Move-Item -Path tran_windows\bin -Destination $loc 32 | 33 | Remove-Item tran_windows* -Recurse -Force 34 | 35 | [System.Environment]::SetEnvironmentVariable("Path", $Env:Path + ";$loc\bin", [System.EnvironmentVariableTarget]::User) 36 | 37 | if (Test-Path -path $loc) { 38 | Write-Host "Thanks for installing Tran! Now Refresh your powershell" -ForegroundColor DarkGreen 39 | Write-Host "If this is your first time using the CLI, be sure to run 'tran --help' first." -ForegroundColor DarkGreen 40 | } else { 41 | Write-Host "Download failed" -ForegroundColor Red 42 | Write-Host "Please try again later" -ForegroundColor Red 43 | } 44 | -------------------------------------------------------------------------------- /scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | installPath=$1 4 | tranPath="" 5 | 6 | if [ "$installPath" != "" ]; then 7 | tranPath=$installPath 8 | else 9 | tranPath=/usr/local/bin 10 | fi 11 | 12 | UNAME=$(uname) 13 | ARCH=$(uname -m) 14 | 15 | rmOldFiles() { 16 | if [ -f $tranPath/tran ]; then 17 | sudo rm -rf $tranPath/tran* 18 | fi 19 | } 20 | 21 | v=$(curl --silent "https://api.github.com/repos/abdfnx/tran/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') 22 | 23 | releases_api_url=https://github.com/abdfnx/tran/releases/download 24 | 25 | successInstall() { 26 | echo "🙏 Thanks for installing Tran! If this is your first time using the CLI, be sure to run `tran --help` first." 27 | } 28 | 29 | mainCheck() { 30 | echo "Installing tran version $v" 31 | name="" 32 | 33 | if [ "$UNAME" == "Linux" ]; then 34 | if [ $ARCH = "x86_64" ]; then 35 | name="tran_linux_${v}_amd64" 36 | elif [ $ARCH = "i686" ]; then 37 | name="tran_linux_${v}_386" 38 | elif [ $ARCH = "i386" ]; then 39 | name="tran_linux_${v}_386" 40 | elif [ $ARCH = "arm64" ]; then 41 | name="tran_linux_${v}_arm64" 42 | elif [ $ARCH = "arm" ]; then 43 | name="tran_linux_${v}_arm" 44 | fi 45 | 46 | tranURL=$releases_api_url/$v/$name.zip 47 | 48 | wget $tranURL 49 | sudo chmod 755 $name.zip 50 | unzip $name.zip 51 | rm $name.zip 52 | 53 | # tran 54 | sudo mv $name/bin/tran $tranPath 55 | 56 | rm -rf $name 57 | 58 | elif [ "$UNAME" == "Darwin" ]; then 59 | if [ $ARCH = "x86_64" ]; then 60 | name="tran_macos_${v}_amd64" 61 | elif [ $ARCH = "arm64" ]; then 62 | name="tran_macos_${v}_arm64" 63 | fi 64 | 65 | tranURL=$releases_api_url/$v/$name.zip 66 | 67 | wget $tranURL 68 | sudo chmod 755 $name.zip 69 | unzip $name.zip 70 | rm $name.zip 71 | 72 | # tran 73 | sudo mv $name/bin/tran $tranPath 74 | 75 | rm -rf $name 76 | 77 | elif [ "$UNAME" == "FreeBSD" ]; then 78 | if [ $ARCH = "x86_64" ]; then 79 | name="tran_freebsd_${v}_amd64" 80 | elif [ $ARCH = "i386" ]; then 81 | name="tran_freebsd_${v}_386" 82 | elif [ $ARCH = "i686" ]; then 83 | name="tran_freebsd_${v}_386" 84 | elif [ $ARCH = "arm64" ]; then 85 | name="tran_freebsd_${v}_arm64" 86 | elif [ $ARCH = "arm" ]; then 87 | name="tran_freebsd_${v}_arm" 88 | fi 89 | 90 | tranURL=$releases_api_url/$v/$name.zip 91 | 92 | wget $tranURL 93 | sudo chmod 755 $name.zip 94 | unzip $name.zip 95 | rm $name.zip 96 | 97 | # tran 98 | sudo mv $name/bin/tran $tranPath 99 | 100 | rm -rf $name 101 | fi 102 | 103 | # chmod 104 | sudo chmod 755 $tranPath/tran 105 | } 106 | 107 | rmOldFiles 108 | mainCheck 109 | 110 | if [ -x "$(command -v tran)" ]; then 111 | successInstall 112 | else 113 | echo "Download failed 😔" 114 | echo "Please try again." 115 | fi 116 | -------------------------------------------------------------------------------- /scripts/shell/README: -------------------------------------------------------------------------------- 1 | Welcome to Tran 👋 2 | 3 | you can do your first steps 4 | 5 | - `$ tran` 6 | - `$ tran send README.md` 7 | - `$ tran receive $CODE` 8 | 9 | run `tran -h` for get help 📄 10 | -------------------------------------------------------------------------------- /scripts/shell/zshrc: -------------------------------------------------------------------------------- 1 | export ZSH="/home/trn/.oh-my-zsh" 2 | 3 | ZSH_THEME="af-magic" 4 | 5 | plugins=( git zsh-syntax-highlighting zsh-autosuggestions ) 6 | 7 | source $ZSH/oh-my-zsh.sh 8 | 9 | alias s="source ~/.zshrc" 10 | alias n="nano ~/.zshrc" 11 | alias update="sudo apt update" 12 | alias upgrade="sudo apt upgrade" 13 | alias py="python3" 14 | -------------------------------------------------------------------------------- /scripts/tag.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | tag="${1}" 4 | 5 | while (($#)); do 6 | case "$2" in 7 | 8 | -a) 9 | git tag -a "${tag}" -m "" 10 | exit 0 11 | ;; 12 | 13 | -p) 14 | git push origin "${tag}" 15 | exit 0 16 | ;; 17 | 18 | -x) 19 | git tag -a "${tag}" -m "" 20 | git push origin "${tag}" 21 | exit 0 22 | ;; 23 | 24 | -d) 25 | git tag -d "${tag}" 26 | git push --delete origin "${tag}" 27 | exit 0 28 | ;; 29 | 30 | esac 31 | done 32 | -------------------------------------------------------------------------------- /tools/errors.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "io" 5 | "fmt" 6 | "net" 7 | "errors" 8 | "strings" 9 | 10 | "github.com/spf13/cobra" 11 | "github.com/AlecAivazis/survey/v2/terminal" 12 | ) 13 | 14 | // FlagError is the kind of error raised in flag processing 15 | type FlagError struct { 16 | Err error 17 | } 18 | 19 | func (fe FlagError) Error() string { 20 | return fe.Err.Error() 21 | } 22 | 23 | func (fe FlagError) Unwrap() error { 24 | return fe.Err 25 | } 26 | 27 | // SilentError is an error that triggers exit code 1 without any error messaging 28 | var SilentError = errors.New("SilentError") 29 | 30 | // CancelError signals user-initiated cancellation 31 | var CancelError = errors.New("CancelError") 32 | 33 | func IsUserCancellation(err error) bool { 34 | return errors.Is(err, CancelError) || errors.Is(err, terminal.InterruptErr) 35 | } 36 | 37 | func MutuallyExclusive(message string, conditions ...bool) error { 38 | numTrue := 0 39 | 40 | for _, ok := range conditions { 41 | if ok { 42 | numTrue++ 43 | } 44 | } 45 | 46 | if numTrue > 1 { 47 | return &FlagError{Err: errors.New(message)} 48 | } 49 | 50 | return nil 51 | } 52 | 53 | func PrintError(out io.Writer, err error, cmd *cobra.Command, debug bool) { 54 | var dnsError *net.DNSError 55 | 56 | if errors.As(err, &dnsError) { 57 | fmt.Fprintf(out, "error connecting to %s\n", dnsError.Name) 58 | 59 | if debug { 60 | fmt.Fprintln(out, dnsError) 61 | } 62 | 63 | return 64 | } 65 | 66 | fmt.Fprintln(out, err) 67 | 68 | var flagError *FlagError 69 | if errors.As(err, &flagError) || strings.HasPrefix(err.Error(), "unknown command ") { 70 | if !strings.HasSuffix(err.Error(), "\n") { 71 | fmt.Fprintln(out) 72 | } 73 | 74 | fmt.Fprintln(out, cmd.UsageString()) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tools/files.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "fmt" 7 | "bufio" 8 | "strings" 9 | "archive/tar" 10 | "path/filepath" 11 | 12 | "github.com/klauspost/pgzip" 13 | "github.com/abdfnx/tran/constants" 14 | ) 15 | 16 | func ReadFiles(fileNames []string) ([]*os.File, error) { 17 | var files []*os.File 18 | 19 | for _, fileName := range fileNames { 20 | f, err := os.Open(fileName) 21 | if err != nil { 22 | return nil, fmt.Errorf("file '%s' not found", fileName) 23 | } 24 | 25 | files = append(files, f) 26 | } 27 | 28 | return files, nil 29 | } 30 | 31 | // ArchiveAndCompressFiles tars and gzip-compresses files into a temporary file, returning it 32 | // along with the resulting size 33 | func ArchiveAndCompressFiles(files []*os.File) (*os.File, int64, error) { 34 | // chained writers -> writing to tw writes to gw -> writes to temporary file 35 | tempFile, err := os.CreateTemp(os.TempDir(), constants.SEND_TEMP_FILE_NAME_PREFIX) 36 | 37 | if err != nil { 38 | return nil, 0, err 39 | } 40 | 41 | tempFileWriter := bufio.NewWriter(tempFile) 42 | gw := pgzip.NewWriter(tempFileWriter) 43 | tw := tar.NewWriter(gw) 44 | 45 | for _, file := range files { 46 | err := addToTarArchive(tw, file) 47 | if err != nil { 48 | return nil, 0, err 49 | } 50 | } 51 | 52 | tw.Close() 53 | gw.Close() 54 | 55 | tempFileWriter.Flush() 56 | fileInfo, err := tempFile.Stat() 57 | 58 | if err != nil { 59 | return nil, 0, err 60 | } 61 | 62 | tempFile.Seek(0, io.SeekStart) 63 | 64 | return tempFile, fileInfo.Size(), nil 65 | } 66 | 67 | // DecompressAndUnarchiveBytes gzip-decompresses and un-tars files into the current working directory 68 | // and returns the names and decompressed size of the created files 69 | func DecompressAndUnarchiveBytes(reader io.Reader) ([]string, int64, error) { 70 | // chained readers -> gr reads from reader -> tr reads from gr 71 | gr, err := pgzip.NewReader(reader) 72 | 73 | if err != nil { 74 | return nil, 0, err 75 | } 76 | 77 | defer gr.Close() 78 | 79 | tr := tar.NewReader(gr) 80 | 81 | var createdFiles []string 82 | var decompressedSize int64 83 | 84 | for { 85 | header, err := tr.Next() 86 | 87 | if err == io.EOF { 88 | break 89 | } 90 | 91 | if err != nil { 92 | return nil, 0, err 93 | } 94 | if header == nil { 95 | continue 96 | } 97 | 98 | cwd, err := os.Getwd() 99 | 100 | if err != nil { 101 | return nil, 0, err 102 | } 103 | 104 | fileTarget := filepath.Join(cwd, header.Name) 105 | 106 | switch header.Typeflag { 107 | case tar.TypeDir: 108 | if _, err := os.Stat(fileTarget); err != nil { 109 | if err := os.MkdirAll(fileTarget, 0755); err != nil { 110 | return nil, 0, err 111 | } 112 | } 113 | 114 | case tar.TypeReg: 115 | f, err := os.OpenFile(fileTarget, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) 116 | 117 | if err != nil { 118 | return nil, 0, err 119 | } 120 | 121 | if _, err := io.Copy(f, tr); err != nil { 122 | return nil, 0, err 123 | } 124 | 125 | fileInfo, err := f.Stat() 126 | 127 | if err != nil { 128 | return nil, 0, err 129 | } 130 | 131 | decompressedSize += fileInfo.Size() 132 | createdFiles = append(createdFiles, header.Name) 133 | 134 | f.Close() 135 | } 136 | } 137 | 138 | return createdFiles, decompressedSize, nil 139 | } 140 | 141 | // Traverses files and directories (recursively) for total size in bytes 142 | func FilesTotalSize(files []*os.File) (int64, error) { 143 | var size int64 144 | 145 | for _, file := range files { 146 | err := filepath.Walk(file.Name(), func(_ string, info os.FileInfo, err error) error { 147 | if err != nil { 148 | return err 149 | } 150 | 151 | if !info.IsDir() { 152 | size += info.Size() 153 | } 154 | 155 | return err 156 | }) 157 | 158 | if err != nil { 159 | return 0, err 160 | } 161 | } 162 | 163 | return size, nil 164 | } 165 | 166 | // source: https://gist.github.com/mimoo/25fc9716e0f1353791f5908f94d6e726 167 | func addToTarArchive(tw *tar.Writer, file *os.File) error { 168 | return filepath.Walk(file.Name(), func(file string, fi os.FileInfo, err error) error { 169 | header, e := tar.FileInfoHeader(fi, file) 170 | if e != nil { 171 | return err 172 | } 173 | 174 | header.Name = filepath.ToSlash(file) 175 | 176 | if err := tw.WriteHeader(header); err != nil { 177 | return err 178 | } 179 | 180 | if !fi.IsDir() { 181 | data, err := os.Open(file) 182 | if err != nil { 183 | return err 184 | } 185 | 186 | defer data.Close() 187 | 188 | if _, err := io.Copy(tw, data); err != nil { 189 | return err 190 | } 191 | } 192 | 193 | return nil 194 | }) 195 | } 196 | 197 | func RemoveTemporaryFiles(prefix string) { 198 | tempFiles, err := os.ReadDir(os.TempDir()) 199 | 200 | if err != nil { 201 | return 202 | } 203 | 204 | for _, tempFile := range tempFiles { 205 | fileInfo, err := tempFile.Info() 206 | if err != nil { 207 | continue 208 | } 209 | 210 | fileName := fileInfo.Name() 211 | if strings.HasPrefix(fileName, prefix) { 212 | os.Remove(filepath.Join(os.TempDir(), fileName)) 213 | } 214 | } 215 | } 216 | 217 | // source: https://yourbasic.org/golang/formatting-byte-size-to-human-readable-format 218 | func ByteCountSI(b int64) string { 219 | const unit = 1000 220 | 221 | if b < unit { 222 | return fmt.Sprintf("%d B", b) 223 | } 224 | 225 | div, exp := int64(unit), 0 226 | 227 | for n := b / unit; n >= unit; n /= unit { 228 | div *= unit 229 | exp++ 230 | } 231 | 232 | return fmt.Sprintf("%.1f %cB", 233 | float64(b)/float64(div), "kMGTPE"[exp]) 234 | } 235 | -------------------------------------------------------------------------------- /tools/json.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "fmt" 5 | "encoding/json" 6 | ) 7 | 8 | func prettyJSONFormat(i interface{}) string { 9 | s, _ := json.MarshalIndent(i, "", " ") 10 | return string(s) 11 | } 12 | 13 | func DecodePayload(payload interface{}, target interface{}) (err error) { 14 | bytes, err := json.Marshal(payload) 15 | 16 | if err != nil { 17 | return fmt.Errorf("could not marshal payload into bytes:%e", err) 18 | } 19 | 20 | err = json.Unmarshal(bytes, &target) 21 | if err != nil { 22 | return fmt.Errorf("faulty payload format\nexpected format:\n%s\ngot:\n%s", 23 | prettyJSONFormat(&target), 24 | prettyJSONFormat(&payload)) 25 | } 26 | 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /tools/math.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "encoding/binary" 5 | mrand "math/rand" 6 | crand "crypto/rand" 7 | ) 8 | 9 | func RandomSeed() { 10 | var b [8]byte 11 | 12 | _, err := crand.Read(b[:]) 13 | 14 | if err != nil { 15 | panic("failed to seed math/rand") 16 | } 17 | 18 | mrand.Seed(int64(binary.LittleEndian.Uint64(b[:]))) 19 | } 20 | -------------------------------------------------------------------------------- /tools/messaging.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "fmt" 5 | "encoding/json" 6 | 7 | "github.com/gorilla/websocket" 8 | "github.com/abdfnx/tran/core/crypt" 9 | "github.com/abdfnx/tran/models/protocol" 10 | ) 11 | 12 | func ReadTranxMessage(wsConn *websocket.Conn, expected protocol.TranxMessageType) (protocol.TranxMessage, error) { 13 | msg := protocol.TranxMessage{} 14 | err := wsConn.ReadJSON(&msg) 15 | 16 | if err != nil { 17 | return protocol.TranxMessage{}, err 18 | } 19 | 20 | if msg.Type != expected { 21 | return protocol.TranxMessage{}, fmt.Errorf("expected message type: %d. Got type: %d", expected, msg.Type) 22 | } 23 | 24 | return msg, nil 25 | } 26 | 27 | func WriteEncryptedMessage(wsConn *websocket.Conn, msg protocol.TransferMessage, crypt *crypt.Crypt) error { 28 | json, err := json.Marshal(msg) 29 | 30 | if err != nil { 31 | return nil 32 | } 33 | 34 | enc, err := crypt.Encrypt(json) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | wsConn.WriteMessage(websocket.BinaryMessage, enc) 40 | 41 | return nil 42 | } 43 | 44 | func ReadEncryptedMessage(wsConn *websocket.Conn, crypt *crypt.Crypt) (protocol.TransferMessage, error) { 45 | _, enc, err := wsConn.ReadMessage() 46 | 47 | if err != nil { 48 | return protocol.TransferMessage{}, err 49 | } 50 | 51 | dec, err := crypt.Decrypt(enc) 52 | if err != nil { 53 | return protocol.TransferMessage{}, err 54 | } 55 | 56 | msg := protocol.TransferMessage{} 57 | err = json.Unmarshal(dec, &msg) 58 | 59 | if err != nil { 60 | return protocol.TransferMessage{}, err 61 | } 62 | 63 | return msg, nil 64 | } 65 | -------------------------------------------------------------------------------- /tools/password.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "math/rand" 7 | "encoding/hex" 8 | "crypto/sha256" 9 | 10 | "github.com/abdfnx/tran/data" 11 | "github.com/abdfnx/tran/models" 12 | ) 13 | 14 | const passwordLength = 4 15 | 16 | // GeneratePassword generates a random password prefixed with the supplied id. 17 | func GeneratePassword(id int) models.Password { 18 | var words []string 19 | hitlistSize := len(data.PasswordList) 20 | 21 | // generate three unique words 22 | for len(words) != passwordLength { 23 | candidateWord := data.PasswordList[rand.Intn(hitlistSize)] 24 | if !Contains(words, candidateWord) { 25 | words = append(words, candidateWord) 26 | } 27 | } 28 | 29 | password := formatPassword(id, words) 30 | return models.Password(password) 31 | } 32 | 33 | func ParsePassword(passStr string) (models.Password, error) { 34 | re := regexp.MustCompile(`^\d+-[a-z]+-[a-z]+-[a-z]+$`) 35 | ok := re.MatchString(passStr) 36 | 37 | if !ok { 38 | return models.Password(""), fmt.Errorf("password: %q is on wrong format", passStr) 39 | } 40 | 41 | return models.Password(passStr), nil 42 | } 43 | 44 | func formatPassword(prefixIndex int, words []string) string { 45 | return fmt.Sprintf("%d-%s-%s-%s", prefixIndex, words[0], words[1], words[2]) 46 | } 47 | 48 | func HashPassword(password models.Password) string { 49 | h := sha256.New() 50 | h.Write([]byte(password)) 51 | 52 | return hex.EncodeToString(h.Sum(nil)) 53 | } 54 | -------------------------------------------------------------------------------- /tools/ports.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import "net" 4 | 5 | func GetOpenPort() (int, error) { 6 | addr, err := net.ResolveTCPAddr("tcp", "localhost:0") 7 | 8 | if err != nil { 9 | return 0, err 10 | } 11 | 12 | listener, err := net.ListenTCP("tcp", addr) 13 | 14 | if err != nil { 15 | return 0, err 16 | } 17 | 18 | defer listener.Close() 19 | 20 | return listener.Addr().(*net.TCPAddr).Port, nil 21 | } 22 | -------------------------------------------------------------------------------- /tools/strings.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "fmt" 5 | "unicode/utf8" 6 | ) 7 | 8 | func Contains(s []string, str string) bool { 9 | for _, v := range s { 10 | if v == str { 11 | return true 12 | } 13 | } 14 | 15 | return false 16 | } 17 | 18 | // ValidateHostname returns an error if the domain name is not valid 19 | // source: https://gist.github.com/chmike/d4126a3247a6d9a70922fc0e8b4f4013 20 | func ValidateHostname(name string) error { 21 | switch { 22 | case len(name) == 0: 23 | return nil 24 | 25 | case len(name) > 255: 26 | return fmt.Errorf("name length is %d, can't exceed 255", len(name)) 27 | } 28 | 29 | var l int 30 | 31 | for i := 0; i < len(name); i++ { 32 | b := name[i] 33 | 34 | if b == '.' { 35 | // check domain labels validity 36 | switch { 37 | case i == l: 38 | return fmt.Errorf("invalid character '%c' at offset %d: label can't begin with a period", b, i) 39 | 40 | case i-l > 63: 41 | return fmt.Errorf("byte length of label '%s' is %d, can't exceed 63", name[l:i], i-l) 42 | 43 | case name[l] == '-': 44 | return fmt.Errorf("label '%s' at offset %d begins with a hyphen", name[l:i], l) 45 | 46 | case name[i-1] == '-': 47 | return fmt.Errorf("label '%s' at offset %d ends with a hyphen", name[l:i], l) 48 | } 49 | 50 | l = i + 1 51 | 52 | continue 53 | } 54 | 55 | if !(b >= 'a' && b <= 'z' || b >= '0' && b <= '9' || b == '-' || b >= 'A' && b <= 'Z') { 56 | // show the printable unicode character starting at byte offset i 57 | c, _ := utf8.DecodeRuneInString(name[i:]) 58 | 59 | if c == utf8.RuneError { 60 | return fmt.Errorf("invalid rune at offset %d", i) 61 | } 62 | 63 | return fmt.Errorf("invalid character '%c' at offset %d", c, i) 64 | } 65 | } 66 | 67 | // check top level domain validity 68 | switch { 69 | case l == len(name): 70 | return fmt.Errorf("missing top level domain, domain can't end with a period") 71 | 72 | case len(name)-l > 63: 73 | return fmt.Errorf("byte length of top level domain '%s' is %d, can't exceed 63", name[l:], len(name)-l) 74 | 75 | case name[l] == '-': 76 | return fmt.Errorf("top level domain '%s' at offset %d begins with a hyphen", name[l:], l) 77 | 78 | case name[len(name)-1] == '-': 79 | return fmt.Errorf("top level domain '%s' at offset %d ends with a hyphen", name[l:], l) 80 | 81 | case name[l] >= '0' && name[l] <= '9': 82 | return fmt.Errorf("top level domain '%s' at offset %d begins with a digit", name[l:], l) 83 | } 84 | 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /tools/text.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | 7 | "github.com/muesli/reflow/ansi" 8 | "github.com/muesli/reflow/truncate" 9 | ) 10 | 11 | var ( 12 | ellipsis = "..." 13 | minWidthForEllipsis = len(ellipsis) + 2 14 | lineRE = regexp.MustCompile(`(?m)^`) 15 | ws = regexp.MustCompile(`\s+`) 16 | ) 17 | 18 | func Indent(s, indent string) string { 19 | if len(strings.TrimSpace(s)) == 0 { 20 | return s 21 | } 22 | 23 | return lineRE.ReplaceAllLiteralString(s, indent) 24 | } 25 | 26 | func ReplaceExcessiveWhitespace(s string) string { 27 | return ws.ReplaceAllString(strings.TrimSpace(s), " ") 28 | } 29 | 30 | // DisplayWidth calculates what the rendered width of a string may be 31 | func DisplayWidth(s string) int { 32 | return ansi.PrintableRuneWidth(s) 33 | } 34 | 35 | // Truncate shortens a string to fit the maximum display width 36 | func Truncate(maxWidth int, s string) string { 37 | w := DisplayWidth(s) 38 | if w <= maxWidth { 39 | return s 40 | } 41 | 42 | tail := "" 43 | if maxWidth >= minWidthForEllipsis { 44 | tail = ellipsis 45 | } 46 | 47 | r := truncate.StringWithTail(s, uint(maxWidth), tail) 48 | if DisplayWidth(r) < maxWidth { 49 | r += " " 50 | } 51 | 52 | return r 53 | } 54 | 55 | // TruncateColumn replaces the first new line character with an ellipsis 56 | // and shortens a string to fit the maximum display width 57 | func TruncateColumn(maxWidth int, s string) string { 58 | if i := strings.IndexAny(s, "\r\n"); i >= 0 { 59 | s = s[:i] + ellipsis 60 | } 61 | 62 | return Truncate(maxWidth, s) 63 | } 64 | -------------------------------------------------------------------------------- /tools/websocket.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/gorilla/websocket" 8 | ) 9 | 10 | type WsHandlerFunc func(*websocket.Conn) 11 | 12 | func WebsocketHandler(wsHandler WsHandlerFunc) http.HandlerFunc { 13 | wsUpgrader := websocket.Upgrader{} 14 | 15 | return func(w http.ResponseWriter, r *http.Request) { 16 | wsConn, err := wsUpgrader.Upgrade(w, r, nil) 17 | 18 | if err != nil { 19 | log.Println("failed to upgrade connection: ", err) 20 | return 21 | } 22 | 23 | defer wsConn.Close() 24 | 25 | wsHandler(wsConn) 26 | } 27 | } 28 | --------------------------------------------------------------------------------