├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── dependabot.yml └── workflows │ └── main.yml ├── .gitignore ├── .goreleaser.yml ├── DEVELOPMENT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── application └── application.go ├── body └── payload.go ├── cmd ├── completion.go ├── config.go ├── qrcp.go ├── receive.go ├── send.go └── version.go ├── config ├── config.go ├── config_test.go ├── migrate.go ├── testdata │ ├── full.yml │ └── qrcp.yml └── util.go ├── docs ├── CNAME ├── LICENSE ├── _config.yml ├── img │ ├── demo.gif │ ├── logo.svg │ ├── mobile-demo.gif │ ├── mobile-screenshot.png │ └── screenshot.png ├── index.md ├── tutorials │ └── secure-transfers-with-mkcert.md └── update-docs.sh ├── go.mod ├── go.sum ├── logger └── logger.go ├── main.go ├── pages └── pages.go ├── qr └── qr.go ├── server ├── server.go ├── tcpkeepalivelistener.go └── util.go ├── util ├── net.go └── util.go └── version └── version.go /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at claudiodangelis@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributions to this project are super welcome, so here's my recommendations: 2 | 3 | - Make sure that the no one is already working on what you are going to fix/implement 4 | 5 | We use the [someone is working on this](https://github.com/claudiodangelis/qrcp/issues?q=is%3Aissue+is%3Aopen+label%3A%22someone+is+working+on+this%22) label to mark issues that are being taken care of by someone, so please have a look before starting coding 6 | 7 | - If you want to take on an open issue, please announce it in the thread 8 | 9 | This does not mean you have to _ask_ first, but it makes sure that there won't be the case where more than one person are fixing the same bug or implementing the same feature without being aware of each other work, which usually results in a someone's time being wasted 10 | 11 | - Discuss implementation before writing the actual code 12 | 13 | If you think what you are going to work on will take some time and effort, I recommend to share your thoughts in the thread first 14 | 15 | - Review pending pull requests 16 | 17 | Help other users by reviewing their code 18 | 19 | - Explain the pull requests 20 | 21 | Help reviewers by explaining how the patch works, what bugs/problems it addresses and _how_ it should be tested 22 | 23 | - Address one problem per pull request 24 | 25 | When possible, avoid submitting a pull request that addresses more than problem 26 | 27 | - Run `go fmt` before submitting the pull request and address `golint` issues 28 | 29 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [claudiodangelis] 2 | custom: ["https://www.paypal.me/claudiodangelis", "https://www.buymeacoffee.com/claudiodangelis"] 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | I'm opening this issue because: 2 | 3 | - [ ] I have found a bug 4 | - [ ] I want to request a feature 5 | - [ ] I have a question 6 | - [ ] Other 7 | 8 | 9 | - My Go version is: _(paste the output of `go version` and remember that qrcp requires at least version 1.8 of Go)_ 10 | - My [GOPATH](https://github.com/golang/go/wiki/GOPATH) is set to: _(paste the output of `echo $GOPATH`)_ 11 | 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: / 5 | schedule: 6 | interval: daily 7 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | tags: [ 'v*' ] 5 | branches: [ main ] 6 | pull_request: 7 | permissions: 8 | contents: write 9 | env: 10 | GOLANG_VERSION: 1.21.x 11 | jobs: 12 | lint: 13 | name: lint 14 | runs-on: ubuntu-24.04 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-go@v5 18 | with: 19 | go-version: ${{ env.GOLANG_VERSION }} 20 | - name: golangci-lint 21 | uses: golangci/golangci-lint-action@v6 22 | with: 23 | version: v1.60 24 | test: 25 | runs-on: ubuntu-24.04 26 | steps: 27 | - uses: actions/checkout@v4 28 | - name: Setup Go 29 | uses: actions/setup-go@v5 30 | with: 31 | go-version: ${{ env.GOLANG_VERSION }} 32 | - name: Install dependencies 33 | run: go get . 34 | - name: Build 35 | run: go build -v ./... 36 | - name: Test with the Go CLI 37 | run: go test ./... 38 | e2e: 39 | needs: [ test, lint ] 40 | runs-on: ${{ matrix.os }} 41 | strategy: 42 | matrix: 43 | os: [ ubuntu-24.04, macos-15, windows-latest ] 44 | steps: 45 | - uses: actions/checkout@v4 46 | - uses: actions/setup-go@v5 47 | with: 48 | go-version: ${{ env.GOLANG_VERSION }} 49 | - name: Run tests on *nix 50 | if: runner.os != 'Windows' 51 | run: | 52 | TEST_CONTENT="hello" 53 | TEST_FILE="/tmp/qrcp-test.txt" 54 | echo $TEST_CONTENT > $TEST_FILE 55 | go build 56 | OS=$(uname) 57 | INTERFACE=lo 58 | if [[ "$OS" == "Darwin" ]]; then 59 | INTERFACE="lo0" 60 | fi 61 | ./qrcp -i $INTERFACE -p 1606 --path test $TEST_FILE > /dev/null 2>&1 & 62 | QRCP_PID=$! 63 | sleep 2 64 | CURL_OUTPUT=$(curl -s http://127.0.0.1:1606/send/test) 65 | kill $QRCP_PID || true 66 | if [[ "${TEST_CONTENT}" != "${CURL_OUTPUT}" ]]; then 67 | exit 1 68 | fi 69 | - name: Run tests on Windows 70 | if: runner.os == 'Windows' 71 | run: | 72 | $TestContent = "hello" 73 | $TestFile = "$env:TEMP\qrcp-test.txt" 74 | $TestContent | Out-File -FilePath $TestFile -Encoding UTF8 75 | go build 76 | $Job = Start-Job -ScriptBlock { 77 | Start-Process -FilePath ./qrcp -ArgumentList "-i", "any", "-p", "1606", "--path", "test", "$env:TEMP\qrcp-test.txt" -NoNewWindow -Wait 78 | } 79 | Start-Sleep -Seconds 2 80 | $Request = Invoke-WebRequest -Uri http://127.0.0.1:1606/send/test 81 | $FileContent = Get-Content -Path $TestFile -Raw 82 | if ($Request.Content -ne $FileContent) { 83 | Write-Host "Expected: $FileContent" 84 | Write-Host "Got: $($Request.Content)" 85 | exit 1 86 | } 87 | release: 88 | runs-on: ubuntu-24.04 89 | needs: [ e2e ] 90 | if: startsWith(github.event.ref, 'refs/tags/') 91 | steps: 92 | - uses: actions/checkout@v4 93 | 94 | - name: Unshallow 95 | run: git fetch --prune --unshallow 96 | 97 | - name: Set up Go 98 | uses: actions/setup-go@v5 99 | with: 100 | go-version: ${{ env.GOLANG_VERSION }} 101 | 102 | - name: Run GoReleaser 103 | uses: goreleaser/goreleaser-action@v6 104 | with: 105 | args: release --clean 106 | version: '~> v2' 107 | env: 108 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 109 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | qr-filetransfer 2 | qrcp 3 | dist 4 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 2 | before: 3 | hooks: 4 | - go mod download 5 | builds: 6 | - env: 7 | - CGO_ENABLED=0 8 | ldflags: 9 | - -s -w -X github.com/claudiodangelis/qrcp/version.version={{.Version}} -X github.com/claudiodangelis/qrcp/version.date={{.Date}} 10 | goos: 11 | - linux 12 | - darwin 13 | - windows 14 | goarch: 15 | - 386 16 | - amd64 17 | - arm 18 | - arm64 19 | goarm: 20 | - 7 21 | ignore: 22 | - goos: darwin 23 | goarch: 386 24 | archives: 25 | - format_overrides: 26 | - goos: windows 27 | formats: [ tar.gz, zip ] 28 | checksum: 29 | name_template: 'checksums.txt' 30 | changelog: 31 | sort: asc 32 | filters: 33 | exclude: 34 | - '^docs:' 35 | - '^test:' 36 | - '^chore:' 37 | release: 38 | footer: | 39 | ## Downloads 40 | | Platform | Download link | 41 | |----------|------------------------------------------------------------------------------------------------------------------------------| 42 | | Linux | [deb package](https://github.com/claudiodangelis/qrcp/releases/download/{{ .Tag }}/qrcp_{{ .Version }}_linux_amd64.deb) | 43 | | Linux | [RPM package](https://github.com/claudiodangelis/qrcp/releases/download/{{ .Tag }}/qrcp_{{ .Version }}_linux_amd64.rpm) | 44 | | macOS | [macOS package](https://github.com/claudiodangelis/qrcp/releases/download/{{ .Tag }}/qrcp_{{ .Version }}_darwin_amd64.tar.gz) | 45 | | Windows | [Windows package](https://github.com/claudiodangelis/qrcp/releases/download/{{ .Tag }}/qrcp_{{ .Version }}_windows_amd64.tar.gz) | 46 | 47 | Refer to the list of assets below for all supported platform. 48 | nfpms: 49 | - homepage: https://qrcp.sh 50 | maintainer: Claudio d'Angelis 51 | description: Transfer files over wifi from your computer to your mobile device by scanning a QR code without leaving the terminal. 52 | license: MIT 53 | formats: 54 | - deb 55 | - rpm 56 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | ## Versioning 4 | 5 | `qrcp` uses [semver](https://semver.org) for releases. 6 | 7 | Version number is defined in `cmd/version.go`. 8 | 9 | ## Releases 10 | 11 | We are using [goreleases](https://goreleaser.com/), [nfpm](https://nfpm.goreleaser.com/) and [Github Actions](https://github.com/features/actions) to build, package and release `qrcp`. 12 | 13 | The relevant files are: 14 | 15 | - .goreleases.yml 16 | - .github/workflows/main.yml 17 | 18 | The release action is triggered when a tag is pushed to the master branch. 19 | 20 | ## Development workflow 21 | 22 | 1. Open a PR 23 | 2. Let someone review it 24 | 3. Squash commits and merge to master 25 | 4. When ready to release, add a tag 26 | 5. Wait for Github Action to process the release 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Claudio d'Angelis 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](docs/img/logo.svg) 2 | 3 | # $ qrcp 4 | 5 | Transfer files over Wi-Fi from your computer to a mobile device by scanning a QR code without leaving the terminal. 6 | 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/claudiodangelis/qrcp)](https://goreportcard.com/report/github.com/claudiodangelis/qrcp) 8 | 9 | You can support development by donating with [![Buy Me A Coffee](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/claudiodangelis). 10 | 11 | Join the **Telegram channel** [qrcp_dev](https://t.me/qrcp_dev) or the [@qrcp_dev](https://twitter.com/qrcp_dev) **Twitter account** for news about the development. 12 | 13 | 14 | ## How does it work? 15 | ![Screenshot](docs/img/screenshot.png) 16 | 17 | `qrcp` binds a web server to the address of your Wi-Fi network interface on a random port and creates a handler for it. The default handler serves the content and exits the program when the transfer is complete. When used to receive files, `qrcp` serves an upload page and handles the transfer. 18 | 19 | The tool prints a QR code that encodes the text: 20 | 21 | ``` 22 | http://{address}:{port}/{random_path} 23 | ``` 24 | 25 | 26 | Most QR apps can detect URLs in decoded text and act accordingly (i.e. open the decoded URL with the default browser), so when the QR code is scanned the content will begin downloading by the mobile browser. 27 | 28 | Send files to mobile: 29 | 30 | ![screenshot](docs/img/demo.gif) 31 | 32 | Receive files from mobile: 33 | 34 | ![Screenshot](docs/img/mobile-demo.gif) 35 | 36 | ## Tutorials 37 | 38 | - [Secure transfers with mkcert](https://qrcp.sh/tutorials/secure-transfers-with-mkcert) 39 | 40 | # Installation 41 | 42 | ## Install the latest development version with Go 43 | 44 | _Note: it requires go 1.18_ 45 | 46 | go install github.com/claudiodangelis/qrcp@latest 47 | 48 | ## Linux 49 | 50 | Download the latest Linux .tar.gz archive from the [Releases](https://github.com/claudiodangelis/qrcp/releases) page, extract it, move the binary to the proper directory, then set execution permissions. 51 | 52 | ```sh 53 | # Extract the archive 54 | tar xf qrcp_0.5.0_linux_x86_64.tar.gz 55 | # Copy the binary 56 | sudo mv qrcp /usr/local/bin 57 | # Set execution permissions 58 | sudo chmod +x /usr/local/bin/qrcp 59 | ``` 60 | 61 | ### Raspberry Pi 62 | 63 | The following ARM releases are available in the [Releases](https://github.com/claudiodangelis/qrcp/releases) page: 64 | 65 | - `armv7` 66 | - `arm64` 67 | 68 | 69 | ### Using a package manager 70 | 71 | #### ArchLinux 72 | 73 | Packages available on AUR: 74 | - [qrcp-bin](https://aur.archlinux.org/packages/qrcp-bin) 75 | - [qrcp](https://aur.archlinux.org/packages/qrcp) 76 | 77 | #### Deb packages (Ubuntu, Debian, etc) 78 | 79 | Download the latest .deb package from the [Releases page](https://github.com/claudiodangelis/qrcp/releases), then run `dpkg`: 80 | 81 | ```sh 82 | sudo dpkg -i qrcp_0.5.0_linux_x86_64.deb 83 | # Confirm it's working: 84 | qrcp version 85 | ``` 86 | 87 | #### RPM packages (CentOS, Fedora, etc) 88 | 89 | Download the latest .rpm package from the [Releases page](https://github.com/claudiodangelis/qrcp/releases), then run `rpm`: 90 | 91 | ```sh 92 | sudo rpm -i qrcp_0.5.0_linux_x86_64.rpm 93 | # Confirm it's working: 94 | qrcp --help 95 | ``` 96 | 97 | ## Windows 98 | 99 | Download the latest Windows .tar.gz archive from the [Releases page](https://github.com/claudiodangelis/qrcp/releases) and extract the EXE file. 100 | 101 | ### Scoop 102 | 103 | If you use [Scoop](https://scoop.sh/) for package management on Windows, you can install qrcp with the following one-liner: 104 | 105 | ``` 106 | scoop install qrcp 107 | ``` 108 | ### Chocolatey 109 | 110 | If you use [Chocolatey](https://community.chocolatey.org/packages/qrcp) for package management on Windows, you can install qrcp with the following one-liner: 111 | 112 | ``` 113 | choco install qrcp 114 | ``` 115 | 116 | ## MacOS 117 | 118 | Download the latest macOS .tar.gz archive from the [Releases page](https://github.com/claudiodangelis/qrcp/releases), extract it, move the binary to the proper directory, then set execution permissions. 119 | 120 | ```sh 121 | # Extract the archive 122 | tar xf qrcp_0.5.0_macOS_x86_64.tar.gz 123 | # Copy the binary 124 | sudo mv qrcp /usr/local/bin 125 | # Set execution permissions 126 | sudo chmod +x /usr/local/bin/qrcp 127 | # Confirm it's working: 128 | qrcp --help 129 | ``` 130 | 131 | ### Homebrew 132 | 133 | If you use [Homebrew](https://brew.sh) for package management on macOS, you can install qrcp with the following one-liner: 134 | 135 | ``` 136 | brew install qrcp 137 | ``` 138 | 139 | # Usage 140 | 141 | ## Send files 142 | 143 | ### Send a file 144 | 145 | ```sh 146 | qrcp MyDocument.pdf 147 | ``` 148 | 149 | ### Send multiple files at once 150 | 151 | When sending multiple files at once, `qrcp` creates a zip archive of the files or folders you want to transfer, and deletes the zip archive once the transfer is complete. 152 | 153 | ```sh 154 | # Multiple files 155 | qrcp MyDocument.pdf IMG0001.jpg 156 | ``` 157 | 158 | ```sh 159 | # A whole folder 160 | qrcp Documents/ 161 | ``` 162 | 163 | 164 | ### Zip a file before transferring it 165 | You can choose to zip a file before transferring it. 166 | 167 | ```sh 168 | qrcp --zip LongVideo.avi 169 | ``` 170 | 171 | 172 | ## Receive files 173 | 174 | When receiving files, `qrcp` serves an "upload page" through which you can choose files from your mobile. 175 | 176 | ### Receive files to the current directory 177 | 178 | ``` 179 | qrcp receive 180 | ``` 181 | 182 | ### Receive files to a specific directory 183 | 184 | ```sh 185 | # Note: the folder must exist 186 | qrcp receive --output=/tmp/dir 187 | ``` 188 | 189 | 190 | ## Configuration 191 | 192 | `qrcp` works without any prior configuration, however, you can choose to configure to use specific values. 193 | 194 | To configure `qrcp` you can create a configuration file inside `$XDG_CONFIG_HOME/qrcp`. 195 | 196 | > Note: On Linux, the `$XDG_CONFIG_HOME` is `.config` under user home directory. 197 | > So, for example, on Linux the configuration file will be `$HOME/.config/qrcp/config.yml`. 198 | > On MacOS, it defaults to `$HOME/Library/Application Support/qrcp/config.yml`` 199 | 200 | > Note: Starting from version 0.10.0, qrcp uses a YAML configuration file instead of the old JSON one. You can automatically migrate the legacy JSON format to the new YAML format by running `qrcp config migrate`. 201 | 202 | | Key | Type | Notes | 203 | |-------------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 204 | | `interface` | String | This value is automatically discovered during the first launch of `qrcp`, you can set it to override the default. You can use the `any` interface to bind the web server to `0.0.0.0`. | 205 | | `bind` | String | This value is used by qrcp to bind the web server to. Note: if this value is set, the `interface` parameter is ignored. | 206 | | `port` | Integer | When this value is not set, `qrcp` will pick a random port at any launch. | 207 | | `path` | String | When this value is not set, `qrcp` will add a random string at the end of URL. | 208 | | `output` | String | Default directory to receive files to. If empty, the current working directory is used. | 209 | | `fqdn` | String | When this value is set, `qrcp` will use it to replace the IP address in the generated URL. | 210 | | `keepAlive` | Bool | Controls whether `qrcp` should quit after transferring the file. Defaults to `false`. | 211 | | `secure` | Bool | Controls whether `qrcp` should use HTTPS instead of HTTP. Defaults to `false` | 212 | | `tls-cert` | String | Path to the TLS certificate. It's only used when `secure: true`. | 213 | | `tls-key` | String | Path to the TLS key. It's only used when `secure: true`. | 214 | | `reversed` | Bool | Reverse QR code (black text on white background)?" true`. | 215 | 216 | 217 | All the configuration parameters can be controlled via environment variables prefixed with `QRCP_`, for example: 218 | - `$QRCP_INTERFACE` 219 | - `$QRCP_PORT` 220 | - `$QRCP_KEEPALIVE` 221 | - _etc_ 222 | 223 | ### Config Wizard 224 | 225 | The `config` command launches a wizard that lets you configure parameters like interface, port, fully-qualified domain name and keep alive. 226 | 227 | ```sh 228 | qrcp config 229 | ``` 230 | 231 | Note: if some network interfaces are not showing up, use the `--list-all-interfaces` flag to suppress the interfaces' filter. 232 | 233 | ```sh 234 | qrcp --list-all-interfaces config 235 | ``` 236 | 237 | 238 | ### Configuration File 239 | 240 | The default configuration file is stored in $XDG_CONFIG_HOME/qrcp/config.yml, however, you can specify the location of the config file by passing the `--config` flag: 241 | 242 | ```sh 243 | qrcp --config /tmp/qrcp.yml MyDocument.pdf 244 | ``` 245 | 246 | ### Port 247 | 248 | By default `qrcp` listens on a random port. Set the `QRCP_PORT` environment variable or pass the `--port` (or `-p`) flag to choose a specific one: 249 | 250 | ```sh 251 | export QRCP_PORT=8080 252 | qrcp MyDocument 253 | ``` 254 | 255 | Or: 256 | 257 | ```sh 258 | qrcp --port 8080 MyDocument.pdf 259 | ``` 260 | 261 | ### Network Interface 262 | 263 | `qrcp` will try to automatically find the suitable network interface to use for the transfers. If more than one suitable interface is found, it asks you to choose one. 264 | 265 | If you want to use a specific interface, pass the `--interface` (or `-i`) flag: 266 | 267 | 268 | 269 | ```sh 270 | # The webserver will be visible by 271 | # all computers on the tun0's interface network 272 | qrcp -i tun0 MyDocument.dpf 273 | ``` 274 | 275 | 276 | You can also use a special interface name, `any`, which binds the web server to `0.0.0.0`, making the web server visible by everyone on any network, even from an external network. 277 | 278 | This is useful when you want to transfer files from your Amazon EC2, Digital Ocean Droplet, Google Cloud Platform Compute Instance or any other VPS. 279 | 280 | ```sh 281 | qrcp -i any MyDocument.pdf 282 | ``` 283 | 284 | ### Bind 285 | 286 | Alternatively to choosing the interface, you can directly specify the address you want `qrcp` to bind the webserver to. 287 | 288 | ```sh 289 | qrcp --bind 10.20.30.40 MyDocument.pdf 290 | ``` 291 | 292 | ### URL 293 | 294 | `qrcp` uses two patterns for the URLs: 295 | 296 | - send: `http://{ip address}:{port}/send/{random path}` 297 | - receive: `http://{ip address}:{port}/receive/{random path}` 298 | 299 | A few options are available that override these patterns. 300 | 301 | 302 | Pass the `--path` flag to use a specific path for URLs, for example: 303 | 304 | ```sh 305 | # The resulting URL will be 306 | # http://{ip address}:{port}/send/x 307 | qrcp --path=x MyDocument.pdf 308 | ``` 309 | 310 | Pass the `--fqdn` (or `-d`) to use a fully qualified domain name instead of the IP. This is useful in combination with `-i any` you are using it from a remote location: 311 | 312 | ```sh 313 | # The resulting URL will be 314 | # http://example.com:8080/send/xYz9 315 | qrcp --fqdn example.com -i any -p 8080 MyRemoteDocument.pdf 316 | ``` 317 | 318 | ### HTTPS 319 | 320 | **qrcp** supports secure file transfers with HTTPS. To enable secure transfers you need a TLS certificate and the associated key. 321 | 322 | You can choose the path to the TLS certificate and keys from the `qrcp config` wizard, or, if you want, you can pass the `--tls-cert` and `--tls-key`: 323 | 324 | ```sh 325 | qrcp --tls-cert /path/to/cert.pem --tls-key /path/to/cert.key MyDocument 326 | ``` 327 | 328 | A `--secure` flag is available too, you can use it to override the default value. 329 | 330 | ### Default output directory 331 | 332 | ### Open in browser 333 | 334 | If you need a QR to be printed outside your terminal, you can pass the `--browser` flag. With this flag, `qrcp` will still print the QR code to the terminal, but it will also open a new window of your default browser to show the QR code. 335 | 336 | ``` 337 | qrcp --browser MyDocument.pdf 338 | ``` 339 | 340 | ### Keep the server alive 341 | 342 | It can be useful to keep the server alive after transferring the file, for example, when you want to transfer the same file to multiple devices. You can use the `--keep-alive` flag for that: 343 | 344 | ```sh 345 | # The server will not shutdown automatically 346 | # after the first transfer 347 | qrcp --keep-alive MyDocument.pdf 348 | ``` 349 | 350 | ## Shell completion scripts 351 | 352 | `qrcp` comes with a built-in `completion` command that generates shell completion scripts. 353 | 354 | ### Bash: 355 | 356 | $ source <(qrcp completion bash) 357 | 358 | To load completions for each session, execute once: 359 | 360 | Linux: 361 | 362 | $ qrcp completion bash > /etc/bash_completion.d/qrcp 363 | 364 | _Note: if you don't want to install completion scripts system-wide, refer to [Bash Completion FAQ](https://github.com/scop/bash-completion/blob/master/README.md)_. 365 | 366 | MacOS: 367 | 368 | $ qrcp completion bash > /usr/local/etc/bash_completion.d/qrcp 369 | 370 | ### Zsh: 371 | 372 | If shell completion is not already enabled in your environment you will need to enable it. You can execute the following once: 373 | 374 | $ echo "autoload -U compinit; compinit" >> ~/.zshrc 375 | 376 | To load completions for each session, execute once: 377 | 378 | $ qrcp completion zsh > "${fpath[1]}/_qrcp" 379 | 380 | You will need to start a new shell for this setup to take effect. 381 | 382 | ### Fish: 383 | 384 | $ qrcp completion fish | source 385 | 386 | To load completions for each session, execute once: 387 | 388 | $ qrcp completion fish > ~/.config/fish/completions/qrcp.fish 389 | 390 | 391 | ## Authors 392 | 393 | **qrcp**, originally called **qr-filetransfer**, started from an idea of [Claudio d'Angelis](claudiodangelis@gmail.com) ([@claudiodangelis](https://t.me/claudiodangelis) on Telegram), the current maintainer, and it's [developed by the community](https://github.com/claudiodangelis/qrcp/graphs/contributors). 394 | 395 | 396 | [Join us!](https://github.com/claudiodangelis/qrcp/fork) 397 | 398 | ## Credits 399 | 400 | Logo is provided by [@arasatasaygin](https://github.com/arasatasaygin) as part of the [openlogos](https://github.com/arasatasaygin/openlogos) initiative, a collection of free logos for open source projects. 401 | 402 | Check out the rules to claim one: [rules of openlogos](https://github.com/arasatasaygin/openlogos#rules). 403 | 404 | Releases are handled with [goreleaser](https://goreleaser.com). 405 | 406 | ## Clones and Similar Projects 407 | 408 | - [qr-fileshare](https://github.com/shivensinha4/qr-fileshare) - A similar idea executed in NodeJS with a React interface. 409 | - [instant-file-transfer](https://github.com/maximumdata/instant-file-transfer) _(Uncredited)_ - Node.js project similar to this 410 | - [qr-filetransfer](https://github.com/sdushantha/qr-filetransfer) - Python clone of this project 411 | - [qr-filetransfer](https://github.com/svenkatreddy/qr-filetransfer) - Another Node.js clone of this project 412 | - [qr-transfer-node](https://github.com/codezoned/qr-transfer-node) - Another Node.js clone of this project 413 | - [QRDELIVER](https://github.com/realdennis/qrdeliver) - Node.js project similar to this 414 | - [qrfile](https://github.com/sgbj/qrfile) - Transfer files by scanning a QR code 415 | - [quick-transfer](https://github.com/CodeMan99/quick-transfer) - Node.js clone of this project 416 | - [share-file-qr](https://github.com/pwalch/share-file-qr) - Python re-implementation of this project 417 | - [share-files](https://github.com/antoaravinth/share-files) _(Uncredited)_ - Yet another Node.js clone of this project 418 | - [ezshare](https://github.com/mifi/ezshare) - Another Node.js two way file sharing tool supporting folders and multiple files 419 | - [local_file_share](https://github.com/woshimanong1990/local_file_share) - _"share local file to other people, OR smartphone download files which is in pc"_ 420 | - [qrcp](https://github.com/pearl2201/qrcp) - a desktop app clone of `qrcp`, writing with C# and .NET Core, work for Windows. 421 | - [swift_file](https://github.com/mateoradman/swift_file) - Rust project inspired by `qrcp`. 422 | - [qrcp-android](https://github.com/ianfixes/qrcp-android) - Android app inspired by `qrcp`. 423 | 424 | ## License 425 | 426 | MIT. See [LICENSE](LICENSE). 427 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | > 0.8.0 | :white_check_mark: | 8 | | < 0.8.0 | :x: | 9 | 10 | 11 | ## Reporting a Vulnerability 12 | 13 | If you have found a security issue, please immediately contact the main maintainer of the project at claudiodangelis@gmail.com. 14 | -------------------------------------------------------------------------------- /application/application.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | type Flags struct { 4 | Quiet bool 5 | KeepAlive bool 6 | ListAllInterfaces bool 7 | Port int 8 | Path string 9 | Interface string 10 | Bind string 11 | FQDN string 12 | Zip bool 13 | Config string 14 | Browser bool 15 | Secure bool 16 | TlsCert string 17 | TlsKey string 18 | Output string 19 | Reversed bool 20 | } 21 | 22 | type App struct { 23 | Flags Flags 24 | Name string 25 | } 26 | 27 | func New() App { 28 | return App{ 29 | Name: "qrcp", 30 | Flags: Flags{}, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /body/payload.go: -------------------------------------------------------------------------------- 1 | package body 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/claudiodangelis/qrcp/util" 8 | ) 9 | 10 | // Body to transfer 11 | type Body struct { 12 | Filename string 13 | Path string 14 | DeleteAfterTransfer bool 15 | } 16 | 17 | // Delete the payload from disk 18 | func (p Body) Delete() error { 19 | return os.RemoveAll(p.Path) 20 | } 21 | 22 | // FromArgs returns a payload from args 23 | func FromArgs(args []string, zipFlag bool) (Body, error) { 24 | shouldzip := len(args) > 1 || zipFlag 25 | var files []string 26 | // Check if content exists 27 | for _, arg := range args { 28 | file, err := os.Stat(arg) 29 | if err != nil { 30 | return Body{}, err 31 | } 32 | // If at least one argument is dir, the content will be zipped 33 | if file.IsDir() { 34 | shouldzip = true 35 | } 36 | files = append(files, arg) 37 | } 38 | // Prepare the content 39 | // TODO: Research cleaner code 40 | var content string 41 | if shouldzip { 42 | zip, err := util.ZipFiles(files) 43 | if err != nil { 44 | return Body{}, err 45 | } 46 | content = zip 47 | } else { 48 | content = args[0] 49 | } 50 | return Body{ 51 | Path: content, 52 | Filename: filepath.Base(content), 53 | DeleteAfterTransfer: shouldzip, 54 | }, nil 55 | } 56 | -------------------------------------------------------------------------------- /cmd/completion.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | // completionCmd represents the completion command 10 | var completionCmd = &cobra.Command{ 11 | Use: "completion [bash|zsh|fish|powershell]", 12 | Short: "Generate completion script", 13 | Long: `To load completions: 14 | 15 | Bash: 16 | 17 | $ source <(qrcp completion bash) 18 | 19 | # To load completions for each session, execute once: 20 | Linux: 21 | $ qrcp completion bash > /etc/bash_completion.d/qrcp 22 | MacOS: 23 | $ qrcp completion bash > /usr/local/etc/bash_completion.d/qrcp 24 | 25 | Zsh: 26 | 27 | # If shell completion is not already enabled in your environment you will need 28 | # to enable it. You can execute the following once: 29 | 30 | $ echo "autoload -U compinit; compinit" >> ~/.zshrc 31 | 32 | # To load completions for each session, execute once: 33 | $ qrcp completion zsh > "${fpath[1]}/_qrcp" 34 | 35 | # You will need to start a new shell for this setup to take effect. 36 | 37 | Fish: 38 | 39 | $ qrcp completion fish | source 40 | 41 | # To load completions for each session, execute once: 42 | $ qrcp completion fish > ~/.config/fish/completions/qrcp.fish 43 | `, 44 | DisableFlagsInUseLine: true, 45 | ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, 46 | Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), 47 | Run: func(cmd *cobra.Command, args []string) { 48 | switch args[0] { 49 | case "bash": 50 | if err := cmd.Root().GenBashCompletion(os.Stdout); err != nil { 51 | panic(err) 52 | } 53 | case "zsh": 54 | if err := cmd.Root().GenZshCompletion(os.Stdout); err != nil { 55 | panic(err) 56 | } 57 | case "fish": 58 | if err := cmd.Root().GenFishCompletion(os.Stdout, true); err != nil { 59 | panic(err) 60 | } 61 | case "powershell": 62 | if err := cmd.Root().GenPowerShellCompletion(os.Stdout); err != nil { 63 | panic(err) 64 | } 65 | } 66 | }, 67 | } 68 | -------------------------------------------------------------------------------- /cmd/config.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/claudiodangelis/qrcp/config" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func configCmdFunc(command *cobra.Command, args []string) error { 11 | return config.Wizard(app) 12 | } 13 | 14 | var configCmd = &cobra.Command{ 15 | Use: "config", 16 | Short: "Configure qrcp", 17 | Long: "Run an interactive configuration wizard for qrcp. With this command you can configure which network interface and port should be used to create the file server.", 18 | Aliases: []string{"c", "cfg"}, 19 | RunE: configCmdFunc, 20 | } 21 | 22 | var migrateCmd = &cobra.Command{ 23 | Use: "migrate", 24 | Short: "Migrate the legacy configuration file", 25 | Long: "Migrate the legacy JSON configuration file to the new YAML format", 26 | Run: func(cmd *cobra.Command, args []string) { 27 | ok, err := config.Migrate(app) 28 | if err != nil { 29 | fmt.Println("error while migrating the legacy JSON configuration file:", err) 30 | } 31 | if ok { 32 | fmt.Println("Legacy JSON configuration file has been successfully deleted") 33 | } 34 | }, 35 | } 36 | -------------------------------------------------------------------------------- /cmd/qrcp.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/claudiodangelis/qrcp/application" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | var app application.App 9 | 10 | func init() { 11 | app = application.New() 12 | rootCmd.AddCommand(sendCmd) 13 | rootCmd.AddCommand(receiveCmd) 14 | rootCmd.AddCommand(configCmd) 15 | rootCmd.AddCommand(versionCmd) 16 | rootCmd.AddCommand(completionCmd) 17 | configCmd.AddCommand(migrateCmd) 18 | // Global command flags 19 | rootCmd.PersistentFlags().BoolVarP(&app.Flags.Quiet, "quiet", "q", false, "only print errors") 20 | rootCmd.PersistentFlags().BoolVarP(&app.Flags.KeepAlive, "keep-alive", "k", false, "keep server alive after transferring") 21 | rootCmd.PersistentFlags().BoolVarP(&app.Flags.ListAllInterfaces, "list-all-interfaces", "l", false, "list all available interfaces when choosing the one to use") 22 | rootCmd.PersistentFlags().IntVarP(&app.Flags.Port, "port", "p", 0, "port to use for the server") 23 | rootCmd.PersistentFlags().StringVar(&app.Flags.Path, "path", "", "path to use. Defaults to a random string") 24 | rootCmd.PersistentFlags().StringVarP(&app.Flags.Interface, "interface", "i", "", "network interface to use for the server") 25 | rootCmd.PersistentFlags().StringVar(&app.Flags.Bind, "bind", "", "address to bind the web server to") 26 | rootCmd.PersistentFlags().StringVarP(&app.Flags.FQDN, "fqdn", "d", "", "fully-qualified domain name to use for the resulting URLs") 27 | rootCmd.PersistentFlags().BoolVarP(&app.Flags.Zip, "zip", "z", false, "zip content before transferring") 28 | rootCmd.PersistentFlags().StringVarP(&app.Flags.Config, "config", "c", "", "path to the config file, defaults to $XDG_CONFIG_HOME/qrcp/config.json") 29 | rootCmd.PersistentFlags().BoolVarP(&app.Flags.Browser, "browser", "b", false, "display the QR code in a browser window") 30 | rootCmd.PersistentFlags().BoolVarP(&app.Flags.Secure, "secure", "s", false, "use https connection") 31 | rootCmd.PersistentFlags().StringVar(&app.Flags.TlsCert, "tls-cert", "", "path to TLS certificate to use with HTTPS") 32 | rootCmd.PersistentFlags().StringVar(&app.Flags.TlsKey, "tls-key", "", "path to TLS private key to use with HTTPS") 33 | rootCmd.PersistentFlags().BoolVarP(&app.Flags.Reversed, "reversed", "r", false, "Reverse QR code (black text on white background)") 34 | // Receive command flags 35 | receiveCmd.PersistentFlags().StringVarP(&app.Flags.Output, "output", "o", "", "output directory for receiving files") 36 | } 37 | 38 | // The root command (`qrcp`) is like a shortcut of the `send` command 39 | var rootCmd = &cobra.Command{ 40 | Use: "qrcp", 41 | Args: cobra.MinimumNArgs(1), 42 | RunE: sendCmdFunc, 43 | SilenceErrors: true, 44 | SilenceUsage: true, 45 | } 46 | 47 | // Execute the root command 48 | func Execute() error { 49 | if err := rootCmd.Execute(); err != nil { 50 | rootCmd.PrintErrf("Error: %v\nRun `qrcp help` for help.\n", err) 51 | return err 52 | } 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /cmd/receive.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/claudiodangelis/qrcp/config" 7 | "github.com/claudiodangelis/qrcp/logger" 8 | "github.com/claudiodangelis/qrcp/qr" 9 | "github.com/claudiodangelis/qrcp/server" 10 | "github.com/eiannone/keyboard" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | func receiveCmdFunc(command *cobra.Command, args []string) error { 15 | log := logger.New(app.Flags.Quiet) 16 | // Load configuration 17 | cfg := config.New(app) 18 | // Create the server 19 | srv, err := server.New(&cfg) 20 | if err != nil { 21 | return err 22 | } 23 | // Sets the output directory 24 | if err := srv.ReceiveTo(cfg.Output); err != nil { 25 | return err 26 | } 27 | // Prints the URL to scan to screen 28 | log.Print(`Scan the following URL with a QR reader to start the file transfer, press CTRL+C or "q" to exit:`) 29 | log.Print(srv.ReceiveURL) 30 | // Renders the QR 31 | qr.RenderString(srv.ReceiveURL, cfg.Reversed) 32 | if app.Flags.Browser { 33 | srv.DisplayQR(srv.ReceiveURL) 34 | } 35 | if err := keyboard.Open(); err == nil { 36 | defer func() { 37 | keyboard.Close() 38 | }() 39 | go func() { 40 | for { 41 | char, key, _ := keyboard.GetKey() 42 | if string(char) == "q" || key == keyboard.KeyCtrlC { 43 | srv.Shutdown() 44 | } 45 | } 46 | }() 47 | } else { 48 | log.Print(fmt.Sprintf("Warning: keyboard not detected: %v", err)) 49 | } 50 | if err := srv.Wait(); err != nil { 51 | return err 52 | } 53 | return nil 54 | } 55 | 56 | var receiveCmd = &cobra.Command{ 57 | Use: "receive", 58 | Aliases: []string{"r"}, 59 | Short: "Receive one or more files", 60 | Long: "Receive one or more files. The destination directory can be set with the config wizard, or by passing the --output flag. If none of the above are set, the current working directory will be used as a destination directory.", 61 | Example: `# Receive files in the current directory 62 | qrcp receive 63 | # Receive files in a specific directory 64 | qrcp receive --output /tmp 65 | `, 66 | RunE: receiveCmdFunc, 67 | } 68 | -------------------------------------------------------------------------------- /cmd/send.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/claudiodangelis/qrcp/body" 7 | "github.com/claudiodangelis/qrcp/config" 8 | "github.com/claudiodangelis/qrcp/logger" 9 | "github.com/claudiodangelis/qrcp/qr" 10 | "github.com/eiannone/keyboard" 11 | 12 | "github.com/claudiodangelis/qrcp/server" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | func sendCmdFunc(command *cobra.Command, args []string) error { 17 | log := logger.New(app.Flags.Quiet) 18 | body, err := body.FromArgs(args, app.Flags.Zip) 19 | if err != nil { 20 | return err 21 | } 22 | cfg := config.New(app) 23 | srv, err := server.New(&cfg) 24 | if err != nil { 25 | return err 26 | } 27 | // Sets the body 28 | srv.Send(body) 29 | log.Print(`Scan the following URL with a QR reader to start the file transfer, press CTRL+C or "q" to exit:`) 30 | log.Print(srv.SendURL) 31 | qr.RenderString(srv.SendURL, cfg.Reversed) 32 | if app.Flags.Browser { 33 | srv.DisplayQR(srv.SendURL) 34 | } 35 | if err := keyboard.Open(); err == nil { 36 | defer func() { 37 | keyboard.Close() 38 | }() 39 | go func() { 40 | for { 41 | char, key, _ := keyboard.GetKey() 42 | if string(char) == "q" || key == keyboard.KeyCtrlC { 43 | srv.Shutdown() 44 | } 45 | } 46 | }() 47 | } else { 48 | log.Print(fmt.Sprintf("Warning: keyboard not detected: %v", err)) 49 | } 50 | if err := srv.Wait(); err != nil { 51 | return err 52 | } 53 | return nil 54 | } 55 | 56 | var sendCmd = &cobra.Command{ 57 | Use: "send", 58 | Short: "Send a file(s) or directories from this host", 59 | Long: "Send a file(s) or directories from this host", 60 | Aliases: []string{"s"}, 61 | Example: `# Send /path/file.gif. Webserver listens on a random port 62 | qrcp send /path/file.gif 63 | # Shorter version: 64 | qrcp /path/file.gif 65 | # Zip file1.gif and file2.gif, then send the zip package 66 | qrcp /path/file1.gif /path/file2.gif 67 | # Zip the content of directory, then send the zip package 68 | qrcp /path/directory 69 | # Send file.gif by creating a webserver on port 8080 70 | qrcp --port 8080 /path/file.gif 71 | `, 72 | Args: cobra.MinimumNArgs(1), 73 | RunE: sendCmdFunc, 74 | } 75 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/claudiodangelis/qrcp/version" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var versionCmd = &cobra.Command{ 11 | Use: "version", 12 | Short: "Print version number and build information.", 13 | Run: func(c *cobra.Command, args []string) { 14 | fmt.Println(version.String()) 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/adrg/xdg" 12 | "github.com/asaskevich/govalidator" 13 | "github.com/claudiodangelis/qrcp/application" 14 | "github.com/claudiodangelis/qrcp/logger" 15 | "github.com/claudiodangelis/qrcp/util" 16 | "github.com/manifoldco/promptui" 17 | "github.com/spf13/viper" 18 | ) 19 | 20 | type Config struct { 21 | Interface string `yaml:",omitempty"` 22 | Port int `yaml:",omitempty"` 23 | Bind string `yaml:",omitempty"` 24 | KeepAlive bool `yaml:",omitempty"` 25 | Path string `yaml:",omitempty"` 26 | Secure bool `yaml:",omitempty"` 27 | TlsKey string `yaml:",omitempty"` 28 | TlsCert string `yaml:",omitempty"` 29 | FQDN string `yaml:",omitempty"` 30 | Output string `yaml:",omitempty"` 31 | Reversed bool `yaml:",omitempty"` 32 | } 33 | 34 | var interactive bool = false 35 | 36 | func New(app application.App) Config { 37 | log := logger.New(app.Flags.Quiet) 38 | v := getViperInstance(app) 39 | var err error 40 | cfg := Config{} 41 | 42 | _, err = os.Stat(v.ConfigFileUsed()) 43 | if os.IsNotExist(err) { 44 | if err := os.MkdirAll(filepath.Dir(v.ConfigFileUsed()), os.ModeDir|os.ModePerm); err != nil { 45 | panic(err) 46 | } 47 | file, err := os.Create(v.ConfigFileUsed()) 48 | if err != nil { 49 | panic(err) 50 | } 51 | defer file.Close() 52 | } 53 | if err := v.ReadInConfig(); err != nil { 54 | panic(fmt.Errorf("fatal error config file: %s", err)) 55 | } 56 | // Load file 57 | cfg.Interface = v.GetString("interface") 58 | cfg.Bind = v.GetString("bind") 59 | cfg.Port = v.GetInt("port") 60 | cfg.KeepAlive = v.GetBool("keepAlive") 61 | cfg.Path = v.GetString("path") 62 | cfg.Secure = v.GetBool("secure") 63 | cfg.TlsKey = v.GetString("tls-key") 64 | cfg.TlsCert = v.GetString("tls-cert") 65 | cfg.FQDN = v.GetString("fqdn") 66 | cfg.Output = v.GetString("output") 67 | cfg.Reversed = v.GetBool("reversed") 68 | 69 | // Override 70 | if app.Flags.Interface != "" { 71 | cfg.Interface = app.Flags.Interface 72 | } 73 | if app.Flags.Bind != "" { 74 | cfg.Bind = app.Flags.Bind 75 | } 76 | if app.Flags.Port != 0 { 77 | cfg.Port = app.Flags.Port 78 | } 79 | if app.Flags.KeepAlive { 80 | cfg.KeepAlive = true 81 | } 82 | if app.Flags.Path != "" { 83 | cfg.Path = app.Flags.Path 84 | } 85 | if app.Flags.Secure { 86 | cfg.Secure = true 87 | } 88 | if app.Flags.TlsKey != "" { 89 | cfg.TlsKey = app.Flags.TlsKey 90 | } 91 | if app.Flags.TlsCert != "" { 92 | cfg.TlsCert = app.Flags.TlsCert 93 | } 94 | if app.Flags.FQDN != "" { 95 | cfg.FQDN = app.Flags.FQDN 96 | } 97 | if app.Flags.Output != "" { 98 | cfg.Output = app.Flags.Output 99 | } 100 | if app.Flags.Reversed { 101 | cfg.Reversed = true 102 | } 103 | 104 | // Discover interface if it's not been set yet 105 | if !interactive { 106 | if cfg.Interface == "" { 107 | cfg.Interface, err = chooseInterface(app.Flags) 108 | if err != nil { 109 | panic(err) 110 | } 111 | v.Set("interface", cfg.Interface) 112 | if err := v.WriteConfig(); err != nil { 113 | log.Print(fmt.Sprintf("Warning: the configuration file could not be saved: %v\n", err)) 114 | } 115 | } 116 | } 117 | 118 | return cfg 119 | } 120 | 121 | func getViperInstance(app application.App) *viper.Viper { 122 | var configType string 123 | var configFile string 124 | v := viper.New() 125 | if app.Flags.Config != "" { 126 | configFile = app.Flags.Config 127 | configType = filepath.Ext(configFile)[1:] 128 | } else { 129 | oldConfigFile := filepath.Join(xdg.ConfigHome, "qrcp", "config.json") 130 | // Check if old configuration file exists 131 | if _, err := os.Stat(oldConfigFile); os.IsNotExist(err) { 132 | configType = "yml" 133 | } else { 134 | configType = "json" 135 | } 136 | configFile = filepath.Join(xdg.ConfigHome, app.Name, fmt.Sprintf("config.%s", configType)) 137 | } 138 | v.SetConfigType(configType) 139 | v.SetConfigFile(configFile) 140 | v.AutomaticEnv() 141 | v.SetEnvPrefix(app.Name) 142 | v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) 143 | return v 144 | } 145 | 146 | func Wizard(app application.App) error { 147 | interactive = true 148 | cfg := New(app) 149 | v := getViperInstance(app) 150 | // Choose interface 151 | var err error 152 | cfg.Interface, err = chooseInterface(app.Flags) 153 | if err != nil { 154 | panic(err) 155 | } 156 | v.Set("interface", cfg.Interface) 157 | if err := v.WriteConfig(); err != nil { 158 | return err 159 | } 160 | // Ask for bind address 161 | validateBind := func(input string) error { 162 | if input == "" { 163 | return nil 164 | } 165 | if !govalidator.IsIPv4(input) { 166 | return errors.New("invalid address") 167 | } 168 | return nil 169 | } 170 | promptBind := promptui.Prompt{ 171 | Validate: validateBind, 172 | Label: "Enter bind address (this will override the chosen interface address)", 173 | Default: cfg.Bind, 174 | } 175 | if promptBindResultString, err := promptBind.Run(); err == nil { 176 | if promptBindResultString != "" { 177 | v.Set("bind", promptBindResultString) 178 | } 179 | } 180 | // Ask for port 181 | validatePort := func(input string) error { 182 | _, err := strconv.ParseUint(input, 10, 16) 183 | if err != nil { 184 | return errors.New("invalid number") 185 | } 186 | return nil 187 | } 188 | promptPort := promptui.Prompt{ 189 | Validate: validatePort, 190 | Label: "Choose port, 0 means random port", 191 | Default: fmt.Sprintf("%d", cfg.Port), 192 | } 193 | if promptPortResultString, err := promptPort.Run(); err == nil { 194 | if port, err := strconv.ParseUint(promptPortResultString, 10, 16); err == nil { 195 | if port > 0 { 196 | v.Set("port", port) 197 | } 198 | } 199 | } 200 | // Ask for fully qualified domain name 201 | validateFqdn := func(input string) error { 202 | if input != "" && !govalidator.IsDNSName(input) { 203 | return errors.New("invalid domain") 204 | } 205 | return nil 206 | } 207 | promptFqdn := promptui.Prompt{ 208 | Validate: validateFqdn, 209 | Label: "Choose fully-qualified domain name", 210 | Default: cfg.FQDN, 211 | } 212 | if promptFqdnString, err := promptFqdn.Run(); err == nil { 213 | if promptFqdnString != "" { 214 | v.Set("fqdn", promptFqdnString) 215 | } 216 | 217 | } 218 | promptPath := promptui.Prompt{ 219 | Label: "Choose URL path, empty means random", 220 | Default: cfg.Path, 221 | } 222 | if promptPathResultString, err := promptPath.Run(); err == nil { 223 | if promptPathResultString != "" { 224 | v.Set("path", promptPathResultString) 225 | } 226 | } 227 | // Ask for keep alive 228 | promptKeepAlive := promptui.Select{ 229 | Items: []string{"No", "Yes"}, 230 | Label: "Should the server keep alive after transferring?", 231 | } 232 | if _, promptKeepAliveResultString, err := promptKeepAlive.Run(); err == nil { 233 | if promptKeepAliveResultString == "Yes" { 234 | v.Set("keepAlive", true) 235 | } 236 | } 237 | // HTTPS 238 | // Ask if path is readable and is a file 239 | pathIsReadableFile := func(input string) error { 240 | if input == "" { 241 | return errors.New("invalid path") 242 | } 243 | path, err := filepath.Abs(util.Expand(input)) 244 | if err != nil { 245 | return err 246 | } 247 | fmt.Println(path) 248 | fileinfo, err := os.Stat(path) 249 | if err != nil { 250 | return err 251 | } 252 | if fileinfo.Mode().IsDir() { 253 | return fmt.Errorf("%s is a directory", input) 254 | } 255 | return nil 256 | } 257 | promptSecure := promptui.Select{ 258 | Items: []string{"No", "Yes"}, 259 | Label: "Should files be securely transferred with HTTPS?", 260 | } 261 | if _, promptSecureResultString, err := promptSecure.Run(); err == nil { 262 | if promptSecureResultString == "Yes" { 263 | v.Set("secure", true) 264 | } 265 | cfg.Secure = v.GetBool("secure") 266 | } 267 | if cfg.Secure { 268 | // TLS Cert 269 | promptTlsCert := promptui.Prompt{ 270 | Label: "Choose TLS certificate path. Empty if not using HTTPS.", 271 | Default: cfg.TlsCert, 272 | Validate: pathIsReadableFile, 273 | } 274 | if promptTlsCertString, err := promptTlsCert.Run(); err == nil { 275 | v.Set("tls-cert", util.Expand(promptTlsCertString)) 276 | } 277 | // TLS key 278 | promptTlsKey := promptui.Prompt{ 279 | Label: "Choose TLS certificate key. Empty if not using HTTPS.", 280 | Default: cfg.TlsKey, 281 | Validate: pathIsReadableFile, 282 | } 283 | if promptTlsKeyString, err := promptTlsKey.Run(); err == nil { 284 | v.Set("tls-key", util.Expand(promptTlsKeyString)) 285 | } 286 | } 287 | validateIsDir := func(input string) error { 288 | if input == "" { 289 | return nil 290 | } 291 | path, err := filepath.Abs(input) 292 | if err != nil { 293 | return err 294 | } 295 | f, err := os.Stat(path) 296 | if err != nil { 297 | return err 298 | } 299 | if !f.IsDir() { 300 | return errors.New("path is not a directory") 301 | } 302 | return nil 303 | } 304 | // Ask for default output directory 305 | promptOutput := promptui.Prompt{ 306 | Label: "Choose default output directory for received files, empty does not set a default", 307 | Default: cfg.Output, 308 | Validate: validateIsDir, 309 | } 310 | if promptOutputResultString, err := promptOutput.Run(); err == nil { 311 | if promptOutputResultString != "" { 312 | output, _ := filepath.Abs(promptOutputResultString) 313 | v.Set("output", output) 314 | } 315 | } 316 | promptReversed := promptui.Select{ 317 | Items: []string{"No", "Yes"}, 318 | Label: "Reverse QR code (black text on white background)?", 319 | } 320 | if _, promptReversedResultString, err := promptReversed.Run(); err == nil { 321 | if promptReversedResultString == "Yes" { 322 | v.Set("reversed", true) 323 | } 324 | cfg.Reversed = v.GetBool("reversed") 325 | } 326 | 327 | return v.WriteConfig() 328 | } 329 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "reflect" 7 | "runtime" 8 | "testing" 9 | 10 | "github.com/claudiodangelis/qrcp/application" 11 | ) 12 | 13 | func TestNew(t *testing.T) { 14 | os.Clearenv() 15 | _, f, _, _ := runtime.Caller(0) 16 | foundIface, err := chooseInterface(application.Flags{}) 17 | if err != nil { 18 | panic(err) 19 | } 20 | testdir := filepath.Join(filepath.Dir(f), "testdata") 21 | tempfile, err := os.CreateTemp("", "qrcp*tmp.yml") 22 | if err != nil { 23 | t.Skip() 24 | } 25 | defer os.Remove(tempfile.Name()) 26 | partialconfig, err := os.CreateTemp("", "qrcp*partial.yml") 27 | if err != nil { 28 | panic(err) 29 | } 30 | defer os.Remove(partialconfig.Name()) 31 | if err := os.WriteFile(partialconfig.Name(), []byte(`port: 9090`), os.ModePerm); err != nil { 32 | panic(err) 33 | } 34 | type args struct { 35 | app application.App 36 | } 37 | tests := []struct { 38 | name string 39 | args args 40 | want Config 41 | }{ 42 | { 43 | "partial", args{ 44 | app: application.App{ 45 | Flags: application.Flags{ 46 | Config: partialconfig.Name(), 47 | }, 48 | }, 49 | }, 50 | Config{ 51 | Interface: foundIface, 52 | Port: 9090, 53 | }, 54 | }, 55 | { 56 | "init", args{ 57 | app: application.App{ 58 | Flags: application.Flags{ 59 | Config: tempfile.Name(), 60 | }, 61 | }, 62 | }, 63 | Config{ 64 | Interface: foundIface, 65 | }, 66 | }, 67 | { 68 | "#2", args{ 69 | app: application.App{ 70 | Flags: application.Flags{ 71 | Config: filepath.Join(testdir, "qrcp.yml"), 72 | }, 73 | }, 74 | }, 75 | Config{ 76 | Interface: foundIface, 77 | }, 78 | }, 79 | { 80 | "#2", args{ 81 | app: application.App{ 82 | Flags: application.Flags{ 83 | Config: filepath.Join(testdir, "full.yml"), 84 | }, 85 | }, 86 | }, 87 | Config{ 88 | Interface: foundIface, 89 | Port: 18080, 90 | KeepAlive: false, 91 | Bind: "10.20.30.40", 92 | Path: "random", 93 | Secure: false, 94 | TlsKey: "/path/to/key", 95 | TlsCert: "/path/to/cert", 96 | FQDN: "mylan.com", 97 | Output: "/path/to/default/output/dir", 98 | Reversed: true, 99 | }, 100 | }, 101 | { 102 | "overrides", args{ 103 | app: application.App{ 104 | Flags: application.Flags{ 105 | Config: filepath.Join(testdir, "full.yml"), 106 | Port: 99999, 107 | }, 108 | }, 109 | }, 110 | Config{ 111 | Interface: foundIface, 112 | Port: 99999, 113 | Bind: "10.20.30.40", 114 | KeepAlive: false, 115 | Path: "random", 116 | Secure: false, 117 | TlsKey: "/path/to/key", 118 | TlsCert: "/path/to/cert", 119 | FQDN: "mylan.com", 120 | Output: "/path/to/default/output/dir", 121 | Reversed: true, 122 | }, 123 | }, 124 | } 125 | for _, tt := range tests { 126 | t.Run(tt.name, func(t *testing.T) { 127 | got := New(tt.args.app) 128 | got.Interface = foundIface 129 | if !reflect.DeepEqual(got, tt.want) { 130 | t.Errorf("New() = %v, want %v", got, tt.want) 131 | } 132 | }) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /config/migrate.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/adrg/xdg" 9 | "github.com/claudiodangelis/qrcp/application" 10 | "gopkg.in/yaml.v2" 11 | ) 12 | 13 | // Migrate function will look for an existing legacy configuration file 14 | // and will migrate it to the new format 15 | func Migrate(app application.App) (bool, error) { 16 | oldConfigFile := filepath.Join(xdg.ConfigHome, "qrcp", "config.json") 17 | newConfigFile := filepath.Join(xdg.ConfigHome, "qrcp", "config.yml") 18 | // Check if old configuration file exists 19 | if _, err := os.Stat(oldConfigFile); os.IsNotExist(err) { 20 | return false, nil 21 | } 22 | oldConfigFileBytes, err := os.ReadFile(oldConfigFile) 23 | if err != nil { 24 | panic(err) 25 | } 26 | var cfg Config 27 | if err := json.Unmarshal(oldConfigFileBytes, &cfg); err != nil { 28 | panic(err) 29 | } 30 | newConfigFileBytes, err := yaml.Marshal(cfg) 31 | if err != nil { 32 | panic(err) 33 | } 34 | if err := os.WriteFile(newConfigFile, newConfigFileBytes, 0644); err != nil { 35 | panic(err) 36 | } 37 | // Delete old file 38 | if err := os.Remove(oldConfigFile); err != nil { 39 | panic(err) 40 | } 41 | return true, nil 42 | } 43 | -------------------------------------------------------------------------------- /config/testdata/full.yml: -------------------------------------------------------------------------------- 1 | interface: __PLACEHOLDER_INTERFACE__ 2 | port: 18080 3 | bind: '10.20.30.40' 4 | keepAlive: false 5 | path: random 6 | secure: false 7 | tls-key: /path/to/key 8 | tls-cert: /path/to/cert 9 | fqdn: mylan.com 10 | output: /path/to/default/output/dir 11 | reversed: true 12 | -------------------------------------------------------------------------------- /config/testdata/qrcp.yml: -------------------------------------------------------------------------------- 1 | interface: __PLACEHOLDER_INTERFACE__ 2 | -------------------------------------------------------------------------------- /config/util.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/claudiodangelis/qrcp/application" 8 | "github.com/claudiodangelis/qrcp/util" 9 | "github.com/manifoldco/promptui" 10 | ) 11 | 12 | func chooseInterface(flags application.Flags) (string, error) { 13 | interfaces, err := util.Interfaces(flags.ListAllInterfaces) 14 | if err != nil { 15 | return "", err 16 | } 17 | if len(interfaces) == 0 { 18 | return "", errors.New("no interfaces found") 19 | } 20 | if len(interfaces) == 1 && !interactive { 21 | for name := range interfaces { 22 | fmt.Printf("only one interface found: %s, using this one\n", name) 23 | return name, nil 24 | } 25 | } 26 | // Map for pretty printing 27 | m := make(map[string]string) 28 | items := []string{} 29 | for name, ip := range interfaces { 30 | label := fmt.Sprintf("%s (%s)", name, ip) 31 | m[label] = name 32 | items = append(items, label) 33 | } 34 | // Add the "any" interface 35 | anyIP := "0.0.0.0" 36 | anyName := "any" 37 | anyLabel := fmt.Sprintf("%s (%s)", anyName, anyIP) 38 | m[anyLabel] = anyName 39 | items = append(items, anyLabel) 40 | prompt := promptui.Select{ 41 | Items: items, 42 | Label: "Choose interface", 43 | } 44 | _, result, err := prompt.Run() 45 | if err != nil { 46 | return "", err 47 | } 48 | return m[result], nil 49 | } 50 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | qrcp.sh -------------------------------------------------------------------------------- /docs/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Claudio d'Angelis 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 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /docs/img/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/claudiodangelis/qrcp/ec2ab282c1f9e0b6dde6f7275799c30637bba1ca/docs/img/demo.gif -------------------------------------------------------------------------------- /docs/img/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/img/mobile-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/claudiodangelis/qrcp/ec2ab282c1f9e0b6dde6f7275799c30637bba1ca/docs/img/mobile-demo.gif -------------------------------------------------------------------------------- /docs/img/mobile-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/claudiodangelis/qrcp/ec2ab282c1f9e0b6dde6f7275799c30637bba1ca/docs/img/mobile-screenshot.png -------------------------------------------------------------------------------- /docs/img/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/claudiodangelis/qrcp/ec2ab282c1f9e0b6dde6f7275799c30637bba1ca/docs/img/screenshot.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | ![Logo](img/logo.svg) 2 | 3 | # $ qrcp 4 | 5 | Transfer files over Wi-Fi from your computer to a mobile device by scanning a QR code without leaving the terminal. 6 | 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/claudiodangelis/qrcp)](https://goreportcard.com/report/github.com/claudiodangelis/qrcp) 8 | 9 | You can support development by donating with [![Buy Me A Coffee](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/claudiodangelis). 10 | 11 | Join the **Telegram channel** [qrcp_dev](https://t.me/qrcp_dev) or the [@qrcp_dev](https://twitter.com/qrcp_dev) **Twitter account** for news about the development. 12 | 13 | 14 | ## How does it work? 15 | ![Screenshot](img/screenshot.png) 16 | 17 | `qrcp` binds a web server to the address of your Wi-Fi network interface on a random port and creates a handler for it. The default handler serves the content and exits the program when the transfer is complete. When used to receive files, `qrcp` serves an upload page and handles the transfer. 18 | 19 | The tool prints a QR code that encodes the text: 20 | 21 | ``` 22 | http://{address}:{port}/{random_path} 23 | ``` 24 | 25 | 26 | Most QR apps can detect URLs in decoded text and act accordingly (i.e. open the decoded URL with the default browser), so when the QR code is scanned the content will begin downloading by the mobile browser. 27 | 28 | Send files to mobile: 29 | 30 | ![screenshot](img/demo.gif) 31 | 32 | Receive files from mobile: 33 | 34 | ![Screenshot](img/mobile-demo.gif) 35 | 36 | ## Tutorials 37 | 38 | - [Secure transfers with mkcert](https://qrcp.sh/tutorials/secure-transfers-with-mkcert) 39 | 40 | # Installation 41 | 42 | ## Install the latest development version with Go 43 | 44 | _Note: it requires go 1.18_ 45 | 46 | go install github.com/claudiodangelis/qrcp@latest 47 | 48 | ## Linux 49 | 50 | Download the latest Linux .tar.gz archive from the [Releases](https://github.com/claudiodangelis/qrcp/releases) page, extract it, move the binary to the proper directory, then set execution permissions. 51 | 52 | ```sh 53 | # Extract the archive 54 | tar xf qrcp_0.5.0_linux_x86_64.tar.gz 55 | # Copy the binary 56 | sudo mv qrcp /usr/local/bin 57 | # Set execution permissions 58 | sudo chmod +x /usr/local/bin/qrcp 59 | ``` 60 | 61 | ### Raspberry Pi 62 | 63 | The following ARM releases are available in the [Releases](https://github.com/claudiodangelis/qrcp/releases) page: 64 | 65 | - `armv7` 66 | - `arm64` 67 | 68 | 69 | ### Using a package manager 70 | 71 | #### ArchLinux 72 | 73 | Packages available on AUR: 74 | - [qrcp-bin](https://aur.archlinux.org/packages/qrcp-bin) 75 | - [qrcp](https://aur.archlinux.org/packages/qrcp) 76 | 77 | #### Deb packages (Ubuntu, Debian, etc) 78 | 79 | Download the latest .deb package from the [Releases page](https://github.com/claudiodangelis/qrcp/releases), then run `dpkg`: 80 | 81 | ```sh 82 | sudo dpkg -i qrcp_0.5.0_linux_x86_64.deb 83 | # Confirm it's working: 84 | qrcp version 85 | ``` 86 | 87 | #### RPM packages (CentOS, Fedora, etc) 88 | 89 | Download the latest .rpm package from the [Releases page](https://github.com/claudiodangelis/qrcp/releases), then run `rpm`: 90 | 91 | ```sh 92 | sudo rpm -i qrcp_0.5.0_linux_x86_64.rpm 93 | # Confirm it's working: 94 | qrcp --help 95 | ``` 96 | 97 | ## Windows 98 | 99 | Download the latest Windows .tar.gz archive from the [Releases page](https://github.com/claudiodangelis/qrcp/releases) and extract the EXE file. 100 | 101 | ### Scoop 102 | 103 | If you use [Scoop](https://scoop.sh/) for package management on Windows, you can install qrcp with the following one-liner: 104 | 105 | ``` 106 | scoop install qrcp 107 | ``` 108 | ### Chocolatey 109 | 110 | If you use [Chocolatey](https://community.chocolatey.org/packages/qrcp) for package management on Windows, you can install qrcp with the following one-liner: 111 | 112 | ``` 113 | choco install qrcp 114 | ``` 115 | 116 | ## MacOS 117 | 118 | Download the latest macOS .tar.gz archive from the [Releases page](https://github.com/claudiodangelis/qrcp/releases), extract it, move the binary to the proper directory, then set execution permissions. 119 | 120 | ```sh 121 | # Extract the archive 122 | tar xf qrcp_0.5.0_macOS_x86_64.tar.gz 123 | # Copy the binary 124 | sudo mv qrcp /usr/local/bin 125 | # Set execution permissions 126 | sudo chmod +x /usr/local/bin/qrcp 127 | # Confirm it's working: 128 | qrcp --help 129 | ``` 130 | 131 | ### Homebrew 132 | 133 | If you use [Homebrew](https://brew.sh) for package management on macOS, you can install qrcp with the following one-liner: 134 | 135 | ``` 136 | brew install qrcp 137 | ``` 138 | 139 | # Usage 140 | 141 | ## Send files 142 | 143 | ### Send a file 144 | 145 | ```sh 146 | qrcp MyDocument.pdf 147 | ``` 148 | 149 | ### Send multiple files at once 150 | 151 | When sending multiple files at once, `qrcp` creates a zip archive of the files or folders you want to transfer, and deletes the zip archive once the transfer is complete. 152 | 153 | ```sh 154 | # Multiple files 155 | qrcp MyDocument.pdf IMG0001.jpg 156 | ``` 157 | 158 | ```sh 159 | # A whole folder 160 | qrcp Documents/ 161 | ``` 162 | 163 | 164 | ### Zip a file before transferring it 165 | You can choose to zip a file before transferring it. 166 | 167 | ```sh 168 | qrcp --zip LongVideo.avi 169 | ``` 170 | 171 | 172 | ## Receive files 173 | 174 | When receiving files, `qrcp` serves an "upload page" through which you can choose files from your mobile. 175 | 176 | ### Receive files to the current directory 177 | 178 | ``` 179 | qrcp receive 180 | ``` 181 | 182 | ### Receive files to a specific directory 183 | 184 | ```sh 185 | # Note: the folder must exist 186 | qrcp receive --output=/tmp/dir 187 | ``` 188 | 189 | 190 | ## Configuration 191 | 192 | `qrcp` works without any prior configuration, however, you can choose to configure to use specific values. 193 | 194 | To configure `qrcp` you can create a configuration file inside `$XDG_CONFIG_HOME/qrcp`. 195 | 196 | > Note: On Linux, the `$XDG_CONFIG_HOME` is `.config` under user home directory. 197 | > So, for example, on Linux the configuration file will be `$HOME/.config/qrcp/config.yml`. 198 | > On MacOS, it defaults to `$HOME/Library/Application Support/qrcp/config.yml`` 199 | 200 | > Note: Starting from version 0.10.0, qrcp uses a YAML configuration file instead of the old JSON one. You can automatically migrate the legacy JSON format to the new YAML format by running `qrcp config migrate`. 201 | 202 | | Key | Type | Notes | 203 | |-------------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 204 | | `interface` | String | This value is automatically discovered during the first launch of `qrcp`, you can set it to override the default. You can use the `any` interface to bind the web server to `0.0.0.0`. | 205 | | `bind` | String | This value is used by qrcp to bind the web server to. Note: if this value is set, the `interface` parameter is ignored. | 206 | | `port` | Integer | When this value is not set, `qrcp` will pick a random port at any launch. | 207 | | `path` | String | When this value is not set, `qrcp` will add a random string at the end of URL. | 208 | | `output` | String | Default directory to receive files to. If empty, the current working directory is used. | 209 | | `fqdn` | String | When this value is set, `qrcp` will use it to replace the IP address in the generated URL. | 210 | | `keepAlive` | Bool | Controls whether `qrcp` should quit after transferring the file. Defaults to `false`. | 211 | | `secure` | Bool | Controls whether `qrcp` should use HTTPS instead of HTTP. Defaults to `false` | 212 | | `tls-cert` | String | Path to the TLS certificate. It's only used when `secure: true`. | 213 | | `tls-key` | String | Path to the TLS key. It's only used when `secure: true`. | 214 | | `reversed` | Bool | Reverse QR code (black text on white background)?" true`. | 215 | 216 | 217 | All the configuration parameters can be controlled via environment variables prefixed with `QRCP_`, for example: 218 | - `$QRCP_INTERFACE` 219 | - `$QRCP_PORT` 220 | - `$QRCP_KEEPALIVE` 221 | - _etc_ 222 | 223 | ### Config Wizard 224 | 225 | The `config` command launches a wizard that lets you configure parameters like interface, port, fully-qualified domain name and keep alive. 226 | 227 | ```sh 228 | qrcp config 229 | ``` 230 | 231 | Note: if some network interfaces are not showing up, use the `--list-all-interfaces` flag to suppress the interfaces' filter. 232 | 233 | ```sh 234 | qrcp --list-all-interfaces config 235 | ``` 236 | 237 | 238 | ### Configuration File 239 | 240 | The default configuration file is stored in $XDG_CONFIG_HOME/qrcp/config.yml, however, you can specify the location of the config file by passing the `--config` flag: 241 | 242 | ```sh 243 | qrcp --config /tmp/qrcp.yml MyDocument.pdf 244 | ``` 245 | 246 | ### Port 247 | 248 | By default `qrcp` listens on a random port. Set the `QRCP_PORT` environment variable or pass the `--port` (or `-p`) flag to choose a specific one: 249 | 250 | ```sh 251 | export QRCP_PORT=8080 252 | qrcp MyDocument 253 | ``` 254 | 255 | Or: 256 | 257 | ```sh 258 | qrcp --port 8080 MyDocument.pdf 259 | ``` 260 | 261 | ### Network Interface 262 | 263 | `qrcp` will try to automatically find the suitable network interface to use for the transfers. If more than one suitable interface is found, it asks you to choose one. 264 | 265 | If you want to use a specific interface, pass the `--interface` (or `-i`) flag: 266 | 267 | 268 | 269 | ```sh 270 | # The webserver will be visible by 271 | # all computers on the tun0's interface network 272 | qrcp -i tun0 MyDocument.dpf 273 | ``` 274 | 275 | 276 | You can also use a special interface name, `any`, which binds the web server to `0.0.0.0`, making the web server visible by everyone on any network, even from an external network. 277 | 278 | This is useful when you want to transfer files from your Amazon EC2, Digital Ocean Droplet, Google Cloud Platform Compute Instance or any other VPS. 279 | 280 | ```sh 281 | qrcp -i any MyDocument.pdf 282 | ``` 283 | 284 | ### Bind 285 | 286 | Alternatively to choosing the interface, you can directly specify the address you want `qrcp` to bind the webserver to. 287 | 288 | ```sh 289 | qrcp --bind 10.20.30.40 MyDocument.pdf 290 | ``` 291 | 292 | ### URL 293 | 294 | `qrcp` uses two patterns for the URLs: 295 | 296 | - send: `http://{ip address}:{port}/send/{random path}` 297 | - receive: `http://{ip address}:{port}/receive/{random path}` 298 | 299 | A few options are available that override these patterns. 300 | 301 | 302 | Pass the `--path` flag to use a specific path for URLs, for example: 303 | 304 | ```sh 305 | # The resulting URL will be 306 | # http://{ip address}:{port}/send/x 307 | qrcp --path=x MyDocument.pdf 308 | ``` 309 | 310 | Pass the `--fqdn` (or `-d`) to use a fully qualified domain name instead of the IP. This is useful in combination with `-i any` you are using it from a remote location: 311 | 312 | ```sh 313 | # The resulting URL will be 314 | # http://example.com:8080/send/xYz9 315 | qrcp --fqdn example.com -i any -p 8080 MyRemoteDocument.pdf 316 | ``` 317 | 318 | ### HTTPS 319 | 320 | **qrcp** supports secure file transfers with HTTPS. To enable secure transfers you need a TLS certificate and the associated key. 321 | 322 | You can choose the path to the TLS certificate and keys from the `qrcp config` wizard, or, if you want, you can pass the `--tls-cert` and `--tls-key`: 323 | 324 | ```sh 325 | qrcp --tls-cert /path/to/cert.pem --tls-key /path/to/cert.key MyDocument 326 | ``` 327 | 328 | A `--secure` flag is available too, you can use it to override the default value. 329 | 330 | ### Default output directory 331 | 332 | ### Open in browser 333 | 334 | If you need a QR to be printed outside your terminal, you can pass the `--browser` flag. With this flag, `qrcp` will still print the QR code to the terminal, but it will also open a new window of your default browser to show the QR code. 335 | 336 | ``` 337 | qrcp --browser MyDocument.pdf 338 | ``` 339 | 340 | ### Keep the server alive 341 | 342 | It can be useful to keep the server alive after transferring the file, for example, when you want to transfer the same file to multiple devices. You can use the `--keep-alive` flag for that: 343 | 344 | ```sh 345 | # The server will not shutdown automatically 346 | # after the first transfer 347 | qrcp --keep-alive MyDocument.pdf 348 | ``` 349 | 350 | ## Shell completion scripts 351 | 352 | `qrcp` comes with a built-in `completion` command that generates shell completion scripts. 353 | 354 | ### Bash: 355 | 356 | $ source <(qrcp completion bash) 357 | 358 | To load completions for each session, execute once: 359 | 360 | Linux: 361 | 362 | $ qrcp completion bash > /etc/bash_completion.d/qrcp 363 | 364 | _Note: if you don't want to install completion scripts system-wide, refer to [Bash Completion FAQ](https://github.com/scop/bash-completion/blob/master/README.md)_. 365 | 366 | MacOS: 367 | 368 | $ qrcp completion bash > /usr/local/etc/bash_completion.d/qrcp 369 | 370 | ### Zsh: 371 | 372 | If shell completion is not already enabled in your environment you will need to enable it. You can execute the following once: 373 | 374 | $ echo "autoload -U compinit; compinit" >> ~/.zshrc 375 | 376 | To load completions for each session, execute once: 377 | 378 | $ qrcp completion zsh > "${fpath[1]}/_qrcp" 379 | 380 | You will need to start a new shell for this setup to take effect. 381 | 382 | ### Fish: 383 | 384 | $ qrcp completion fish | source 385 | 386 | To load completions for each session, execute once: 387 | 388 | $ qrcp completion fish > ~/.config/fish/completions/qrcp.fish 389 | 390 | 391 | ## Authors 392 | 393 | **qrcp**, originally called **qr-filetransfer**, started from an idea of [Claudio d'Angelis](claudiodangelis@gmail.com) ([@claudiodangelis](https://t.me/claudiodangelis) on Telegram), the current maintainer, and it's [developed by the community](https://github.com/claudiodangelis/qrcp/graphs/contributors). 394 | 395 | 396 | [Join us!](https://github.com/claudiodangelis/qrcp/fork) 397 | 398 | ## Credits 399 | 400 | Logo is provided by [@arasatasaygin](https://github.com/arasatasaygin) as part of the [openlogos](https://github.com/arasatasaygin/openlogos) initiative, a collection of free logos for open source projects. 401 | 402 | Check out the rules to claim one: [rules of openlogos](https://github.com/arasatasaygin/openlogos#rules). 403 | 404 | Releases are handled with [goreleaser](https://goreleaser.com). 405 | 406 | ## Clones and Similar Projects 407 | 408 | - [qr-fileshare](https://github.com/shivensinha4/qr-fileshare) - A similar idea executed in NodeJS with a React interface. 409 | - [instant-file-transfer](https://github.com/maximumdata/instant-file-transfer) _(Uncredited)_ - Node.js project similar to this 410 | - [qr-filetransfer](https://github.com/sdushantha/qr-filetransfer) - Python clone of this project 411 | - [qr-filetransfer](https://github.com/svenkatreddy/qr-filetransfer) - Another Node.js clone of this project 412 | - [qr-transfer-node](https://github.com/codezoned/qr-transfer-node) - Another Node.js clone of this project 413 | - [QRDELIVER](https://github.com/realdennis/qrdeliver) - Node.js project similar to this 414 | - [qrfile](https://github.com/sgbj/qrfile) - Transfer files by scanning a QR code 415 | - [quick-transfer](https://github.com/CodeMan99/quick-transfer) - Node.js clone of this project 416 | - [share-file-qr](https://github.com/pwalch/share-file-qr) - Python re-implementation of this project 417 | - [share-files](https://github.com/antoaravinth/share-files) _(Uncredited)_ - Yet another Node.js clone of this project 418 | - [ezshare](https://github.com/mifi/ezshare) - Another Node.js two way file sharing tool supporting folders and multiple files 419 | - [local_file_share](https://github.com/woshimanong1990/local_file_share) - _"share local file to other people, OR smartphone download files which is in pc"_ 420 | - [qrcp](https://github.com/pearl2201/qrcp) - a desktop app clone of `qrcp`, writing with C# and .NET Core, work for Windows. 421 | - [swift_file](https://github.com/mateoradman/swift_file) - Rust project inspired by `qrcp`. 422 | ## License 423 | 424 | MIT. See [LICENSE](LICENSE). 425 | -------------------------------------------------------------------------------- /docs/tutorials/secure-transfers-with-mkcert.md: -------------------------------------------------------------------------------- 1 | # Secure transfers with mkcert 2 | 3 | _Published on October 14th, 2020, by [Claudio d'Angelis](https://claudiodangelis.com)._ 4 | 5 | 6 | In this tutorial you will learn how to securely transfer files with qrcp by creating a local Certificate Authority using [mkcert](https://github.com/FiloSottile/mkcert) and generating a certificate for `qrcp`. From its README, **mkcert** _is a simple tool for making locally-trusted development certificates. It requires no configuration._ 7 | 8 | The following values will be used for this tutorial, they refer to a typical Linux system and may differ for you according to your operating system: 9 | 10 | | Name | Value | 11 | | --- | --- | 12 | | IP of the computer/laptop | `192.168.1.107` | 13 | | Root certificate path | `~/.local/share/mkcert/rootCA.pem` | 14 | | Certificate path | `~/certs/192.168.1.107.pem` | 15 | | Certificate key | `~/certs/192.168.1.107-key.pem` | 16 | | Transferred file | `MyDocument.pdf` | 17 | 18 | **Note**: secure transfers are only supported by `qrcp` version `0.7.0` and above. 19 | 20 | ## Generate certificates 21 | 22 | Open your terminal, create a directory called `certs` and change to it 23 | Install **mkcert** _(refer to the [README](https://github.com/FiloSottile/mkcert#installation))_. 24 | 25 | Generate the Certificate Authority: 26 | ``` 27 | mkcert --install 28 | ``` 29 | 30 | If everything worked correctly, you should see the similar output: 31 | ``` 32 | Created a new local CA at "/home/me/.local/share/mkcert" 33 | Sudo password: 34 | The local CA is now installed in the system trust store! ⚡️ 35 | The local CA is now installed in the Firefox and/or Chrome/Chromium trust store (requires browser restart)! 36 | ``` 37 | 38 | Generate a certificate for the IP of your computer/laptop: 39 | 40 | ``` 41 | mkcert 192.168.1.107 42 | ``` 43 | You should see a similar output: 44 | 45 | ``` 46 | Using the local CA at "/home/me/.local/share/mkcert" 47 | 48 | Created a new certificate valid for the following names 49 | - "192.168.1.107" 50 | 51 | The certificate is at "./192.168.1.107.pem" and the key at "./192.168.1.107-key.pem" 52 | 53 | ``` 54 | 55 | At this point you should securely upload the root certificate generated by mkcert to your mobile phone. You have a few options for this, the simplest is just sending it by email. You can check the location of the root certificate's parent directory by running: 56 | 57 | ``` 58 | mkcert --CAROOT 59 | ``` 60 | 61 | If you are using **iOS**, you should _trust_ the certificate authority, you can find more information here: [Trust manually installed certificate profiles in iOS and iPadOS](https://support.apple.com/en-nz/HT204477). 62 | 63 | If you are using **Android**, you must convert the root certificate from PEM format to DER format. Note that you may need to install the `openssl` tool. 64 | 65 | ``` 66 | openssl x509 -inform PEM -outform DER -in $(mkcert --CAROOT)/rootCA.pem -out $(mkcert --CAROOT)/rootCA.der.crt 67 | ``` 68 | 69 | Upload the converted certificate to your Android device, and install it by simply opening the file. When asked to enter the certificate name, you can enter a friendly name, for example "mkcert". 70 | 71 | ## Transfer something! 72 | 73 | You are now ready to securely transfer a file using the certificates generated by mkcert: 74 | 75 | ``` 76 | qrcp --tls-cert ~/certs/192.168.1.107.pem --tls-key ~/certs/192.168.1.107-key.pem screenshot.png 77 | ``` 78 | 79 | The output will be something like this (note that the printed URL starts with "https"): 80 | ``` 81 | Scan the following URL with a QR reader to start the file transfer: 82 | https://192.168.1.107:40221/send/ljd7 83 | █████████████████████████████████████ 84 | █████████████████████████████████████ 85 | ████ ▄▄▄▄▄ █ █▀▀ █▀ ▄█▄▀▀█ ▄▄▄▄▄ ████ 86 | ████ █ █ █▀█▄▄▄▄▄▄█▄█▄██ █ █ ████ 87 | ████ █▄▄▄█ ██▀▄▄▀▄▀█▀█ ▀▄█ █▄▄▄█ ████ 88 | ████▄▄▄▄▄▄▄█ ▀▄█ ▀ ▀ ▀ █ █▄▄▄▄▄▄▄████ 89 | ████ ▀▄ ▀▄▄ ▀██▀▄▄▄▄ ▄█▀▄██▄█▄ ████ 90 | ████▀▀ █▄▀▀ ▄▄█▀ █▀▄█▄▀ ▄▄██▄▄▀████ 91 | ████▀█ ▀█▄▄█ ▄██ ▀██▀▀ ▄█▀▀█▄ ▀▀████ 92 | ████ █▀█▀▄ █ ▄ ▀▄ ▀▄██ █▀█▄ ▄████ 93 | █████ ▀ ▀▀▄ ▄▀█ ▀██▄█▄▀▄██ ▀ ▀ █████ 94 | ████▄████▄▄ ▀█ ▀████▄█▄ ▄▀█ █ ██████ 95 | █████▄█▄▄▄▄█▀ ▀█ ▀ ▀▀ ▄▄▄ ▀▀████ 96 | ████ ▄▄▄▄▄ █ █▄▄▄█ █▀█ █▄█ █▄▀████ 97 | ████ █ █ █▀ ▀ ▄▀ █ █▀ ▄ ▄▀▄▄▄████ 98 | ████ █▄▄▄█ █▄█▄█ ▀▀▀▄▀ █▄▄ █▀█▄▀▄████ 99 | ████▄▄▄▄▄▄▄█▄▄▄██▄▄▄█▄▄██▄█▄█▄▄▄█████ 100 | █████████████████████████████████████ 101 | ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ 102 | ``` 103 | 104 | Scan the QR and the file will be securely transferred to your mobile. Congratulations! 105 | 106 | ## Configure qrcp to default to those value 107 | 108 | If you want to make this setup persistent so that all transfers will be secured by default, run the wizard with `qrcp config` and make sure the choose the right values to the following questions: 109 | 110 | _(**Note**: you must enter the **absolute path** of the certificates)_ 111 | 112 | - Should files be securely transferred with HTTPS? 113 | **Yes** 114 | - Choose TLS certificate path. Empty if not using HTTPS 115 | **/home/user/certs/192.168.1.107.pem** 116 | - Choose TLS certificate key. Empty if not using HTTPS 117 | **/home/user/certs/192.168.1.107-key.pem** 118 | 119 | After configuring qrcp, all transfers will be always secured. 120 | If you'll want to temporarily disable security, just add the `--secure=false` flag. 121 | 122 | Secure transfer: 123 | ``` 124 | qrcp MyDocument.pdf 125 | ``` 126 | 127 | Insecure transfer: 128 | ``` 129 | qrcp --secure=false MyDocument.pdf 130 | ``` 131 | 132 | ## Conclusion 133 | 134 | In this tutorial we have seen how to: 135 | - create a local certificate authority with `mkcert` 136 | - issue a certificate for your computer/laptop 137 | - use that certificate to secure the transferring of a file with `qrcp` 138 | 139 | If you want to learn more about HTTPS and secure connections, have a look at the [How HTTPS works ...in a comic!](https://howhttps.works/) website. 140 | 141 | ## Useful Links 142 | 143 | - [qrcp homepage](https://github.com/claudiodangelis/qrcp) 144 | - [mkcert homepage](https://github.com/FiloSottile/mkcert) 145 | - [qrcp Telegram channel](https://t.me/qrcp_dev) -------------------------------------------------------------------------------- /docs/update-docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if ! [[ $(git rev-parse --show-toplevel 2>/dev/null) = "$PWD" ]]; then 3 | echo "error: script should be run from the root of the repository" 4 | exit 1 5 | fi 6 | cp README.md docs/index.md 7 | find docs -name "*.md" | while read page 8 | do 9 | sed -i 's/docs\/img/img/g' $page 10 | done 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/claudiodangelis/qrcp 2 | 3 | go 1.21.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/adrg/xdg v0.5.3 9 | github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 10 | github.com/eiannone/keyboard v0.0.0-20200508000154-caf4b762e807 11 | github.com/glendc/go-external-ip v0.1.0 12 | github.com/jhoonb/archivex v0.0.0-20180718040744-0488e4ce1681 13 | github.com/manifoldco/promptui v0.9.0 14 | github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086 15 | github.com/spf13/cobra v1.9.1 16 | github.com/spf13/viper v1.20.0 17 | gopkg.in/cheggaaa/pb.v1 v1.0.28 18 | gopkg.in/yaml.v2 v2.4.0 19 | ) 20 | 21 | require ( 22 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect 23 | github.com/fatih/color v1.18.0 // indirect 24 | github.com/fsnotify/fsnotify v1.8.0 // indirect 25 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 26 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 27 | github.com/mattn/go-colorable v0.1.14 // indirect 28 | github.com/mattn/go-runewidth v0.0.9 // indirect 29 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 30 | github.com/sagikazarmark/locafero v0.7.0 // indirect 31 | github.com/sourcegraph/conc v0.3.0 // indirect 32 | github.com/spf13/afero v1.12.0 // indirect 33 | github.com/spf13/cast v1.7.1 // indirect 34 | github.com/spf13/pflag v1.0.6 // indirect 35 | github.com/subosito/gotenv v1.6.0 // indirect 36 | go.uber.org/atomic v1.9.0 // indirect 37 | go.uber.org/multierr v1.9.0 // indirect 38 | golang.org/x/sys v0.29.0 // indirect 39 | golang.org/x/text v0.21.0 // indirect 40 | gopkg.in/yaml.v3 v3.0.1 // indirect 41 | ) 42 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= 2 | github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= 3 | github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06GQ59hwDQAvmK1qxOQGB3WuVTRoY0okPTAv0= 4 | github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= 5 | github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= 6 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 7 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= 8 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 9 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= 10 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 11 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 12 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 14 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/eiannone/keyboard v0.0.0-20200508000154-caf4b762e807 h1:jdjd5e68T4R/j4PWxfZqcKY8KtT9oo8IPNVuV4bSXDQ= 16 | github.com/eiannone/keyboard v0.0.0-20200508000154-caf4b762e807/go.mod h1:Xoiu5VdKMvbRgHuY7+z64lhu/7lvax/22nzASF6GrO8= 17 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 18 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 19 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 20 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 21 | github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= 22 | github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 23 | github.com/glendc/go-external-ip v0.1.0 h1:iX3xQ2Q26atAmLTbd++nUce2P5ht5P4uD4V7caSY/xg= 24 | github.com/glendc/go-external-ip v0.1.0/go.mod h1:CNx312s2FLAJoWNdJWZ2Fpf5O4oLsMFwuYviHjS4uJE= 25 | github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= 26 | github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 27 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 28 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 29 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 30 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 31 | github.com/jhoonb/archivex v0.0.0-20180718040744-0488e4ce1681 h1:EiEjLram6Y0WXygV4WyzKmTr3XaR4CD3tvjdTrsk3cU= 32 | github.com/jhoonb/archivex v0.0.0-20180718040744-0488e4ce1681/go.mod h1:GN1Mg/uXQ6qwXA0HypnUO3xlcQJS9/y68EsHNeuuRa4= 33 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 34 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 35 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 36 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 37 | github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= 38 | github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= 39 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 40 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 41 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 42 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 43 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= 44 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 45 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 46 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 47 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 48 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 49 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 50 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 51 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 52 | github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= 53 | github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= 54 | github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086 h1:RYiqpb2ii2Z6J4x0wxK46kvPBbFuZcdhS+CIztmYgZs= 55 | github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo= 56 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 57 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 58 | github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= 59 | github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= 60 | github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= 61 | github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 62 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 63 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 64 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 65 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 66 | github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY= 67 | github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= 68 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 69 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 70 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 71 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 72 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 73 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 74 | go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= 75 | go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 76 | go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= 77 | go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= 78 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 79 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 80 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 81 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 82 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 83 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 84 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 85 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 86 | gopkg.in/cheggaaa/pb.v1 v1.0.28 h1:n1tBJnnK2r7g9OW2btFH91V92STTUevLXYFb8gy9EMk= 87 | gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= 88 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 89 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 90 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 91 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 92 | -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Print prints its argument if the --quiet flag is not passed 8 | func (l Logger) Print(args ...interface{}) { 9 | if !l.quiet { 10 | fmt.Println(args...) 11 | } 12 | } 13 | 14 | // Logger struct 15 | type Logger struct { 16 | quiet bool 17 | } 18 | 19 | // New logger 20 | func New(quiet bool) Logger { 21 | return Logger{ 22 | quiet: quiet, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/claudiodangelis/qrcp/cmd" 7 | ) 8 | 9 | func main() { 10 | if err := cmd.Execute(); err != nil { 11 | os.Exit(1) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /qr/qr.go: -------------------------------------------------------------------------------- 1 | package qr 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "log" 7 | 8 | "github.com/skip2/go-qrcode" 9 | ) 10 | 11 | // RenderString as a QR code 12 | func RenderString(s string, inverseColor bool) { 13 | q, err := qrcode.New(s, qrcode.Medium) 14 | if err != nil { 15 | log.Fatal(err) 16 | } 17 | fmt.Println(q.ToSmallString(inverseColor)) 18 | } 19 | 20 | // RenderImage returns a QR code as an image.Image 21 | func RenderImage(s string) image.Image { 22 | q, err := qrcode.New(s, qrcode.Medium) 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | return q.Image(256) 27 | } 28 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "fmt" 7 | "image/jpeg" 8 | "io" 9 | "log" 10 | "net" 11 | "net/http" 12 | "net/url" 13 | "os" 14 | "os/exec" 15 | "os/signal" 16 | "path/filepath" 17 | "runtime" 18 | "strings" 19 | "sync" 20 | 21 | "github.com/claudiodangelis/qrcp/qr" 22 | 23 | "github.com/claudiodangelis/qrcp/body" 24 | "github.com/claudiodangelis/qrcp/config" 25 | "github.com/claudiodangelis/qrcp/pages" 26 | "github.com/claudiodangelis/qrcp/util" 27 | "gopkg.in/cheggaaa/pb.v1" 28 | ) 29 | 30 | // Server is the server 31 | type Server struct { 32 | BaseURL string 33 | // SendURL is the URL used to send the file 34 | SendURL string 35 | // ReceiveURL is the URL used to Receive the file 36 | ReceiveURL string 37 | instance *http.Server 38 | body body.Body 39 | outputDir string 40 | stopChannel chan bool 41 | // expectParallelRequests is set to true when qrcp sends files, in order 42 | // to support downloading of parallel chunks 43 | expectParallelRequests bool 44 | } 45 | 46 | // ReceiveTo sets the output directory 47 | func (s *Server) ReceiveTo(dir string) error { 48 | output, err := filepath.Abs(dir) 49 | if err != nil { 50 | return err 51 | } 52 | // Check if the output dir exists 53 | fileinfo, err := os.Stat(output) 54 | if err != nil { 55 | return err 56 | } 57 | if !fileinfo.IsDir() { 58 | return fmt.Errorf("%s is not a valid directory", output) 59 | } 60 | s.outputDir = output 61 | return nil 62 | } 63 | 64 | // Send adds a handler for sending the file 65 | func (s *Server) Send(p body.Body) { 66 | s.body = p 67 | s.expectParallelRequests = true 68 | } 69 | 70 | // DisplayQR creates a handler for serving the QR code in the browser 71 | func (s *Server) DisplayQR(url string) { 72 | const PATH = "/qr" 73 | qrImg := qr.RenderImage(url) 74 | http.HandleFunc(PATH, func(w http.ResponseWriter, r *http.Request) { 75 | w.Header().Set("Content-Type", "image/jpeg") 76 | if err := jpeg.Encode(w, qrImg, nil); err != nil { 77 | panic(err) 78 | } 79 | }) 80 | openBrowser(s.BaseURL + PATH) 81 | } 82 | 83 | // Wait for transfer to be completed, it waits forever if kept awlive 84 | func (s Server) Wait() error { 85 | <-s.stopChannel 86 | if err := s.instance.Shutdown(context.Background()); err != nil { 87 | log.Println(err) 88 | } 89 | if s.body.DeleteAfterTransfer { 90 | if err := s.body.Delete(); err != nil { 91 | panic(err) 92 | } 93 | } 94 | return nil 95 | } 96 | 97 | // Shutdown the server 98 | func (s Server) Shutdown() { 99 | s.stopChannel <- true 100 | } 101 | 102 | // New instance of the server 103 | func New(cfg *config.Config) (*Server, error) { 104 | 105 | app := &Server{} 106 | // Get the address of the configured interface to bind the server to. 107 | // If `bind` configuration parameter has been configured, it takes precedence 108 | bind, err := util.GetInterfaceAddress(cfg.Interface) 109 | if err != nil { 110 | return &Server{}, err 111 | } 112 | if cfg.Bind != "" { 113 | bind = cfg.Bind 114 | } 115 | // Create a listener. If `port: 0`, a random one is chosen 116 | listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", bind, cfg.Port)) 117 | if err != nil { 118 | return nil, err 119 | } 120 | // Set the value of computed port 121 | port := listener.Addr().(*net.TCPAddr).Port 122 | // Set the host 123 | host := fmt.Sprintf("%s:%d", bind, port) 124 | // Get a random path to use 125 | path := cfg.Path 126 | if path == "" { 127 | path = util.GetRandomURLPath() 128 | } 129 | // Set the hostname 130 | hostname := fmt.Sprintf("%s:%d", bind, port) 131 | // Use external IP when using `interface: any`, unless a FQDN is set 132 | if bind == "0.0.0.0" && cfg.FQDN == "" { 133 | fmt.Println("Retrieving the external IP...") 134 | extIP, err := util.GetExternalIP() 135 | if err != nil { 136 | panic(err) 137 | } 138 | extIPString := extIP.String() 139 | fmtstring := "%s:%d" 140 | if strings.Count(extIPString, ":") >= 2 { 141 | // IPv6 address, wrap it in [] to add a port 142 | fmtstring = "[%s]:%d" 143 | } 144 | hostname = fmt.Sprintf(fmtstring, extIPString, port) 145 | } 146 | // Use a fully-qualified domain name if set 147 | if cfg.FQDN != "" { 148 | hostname = fmt.Sprintf("%s:%d", cfg.FQDN, port) 149 | } 150 | // Set URLs 151 | protocol := "http" 152 | if cfg.Secure { 153 | protocol = "https" 154 | } 155 | app.BaseURL = fmt.Sprintf("%s://%s", protocol, hostname) 156 | app.SendURL = fmt.Sprintf("%s/send/%s", 157 | app.BaseURL, path) 158 | app.ReceiveURL = fmt.Sprintf("%s/receive/%s", 159 | app.BaseURL, path) 160 | // Create a server 161 | httpserver := &http.Server{ 162 | Addr: host, 163 | TLSConfig: &tls.Config{ 164 | MinVersion: tls.VersionTLS12, 165 | CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256}, 166 | PreferServerCipherSuites: true, 167 | CipherSuites: []uint16{ 168 | tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, 169 | tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, 170 | tls.TLS_RSA_WITH_AES_256_GCM_SHA384, 171 | tls.TLS_RSA_WITH_AES_256_CBC_SHA, 172 | }, 173 | }, 174 | TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)), 175 | } 176 | // Create channel to send message to stop server 177 | app.stopChannel = make(chan bool) 178 | // Create cookie used to verify request is coming from first client to connect 179 | cookie := http.Cookie{Name: "qrcp", Value: ""} 180 | // Gracefully shutdown when an OS signal is received or when "q" is pressed 181 | sig := make(chan os.Signal, 1) 182 | signal.Notify(sig, os.Interrupt) 183 | go func() { 184 | <-sig 185 | app.stopChannel <- true 186 | }() 187 | // The handler adds and removes from the sync.WaitGroup 188 | // When the group is zero all requests are completed 189 | // and the server is shutdown 190 | var waitgroup sync.WaitGroup 191 | waitgroup.Add(1) 192 | var initCookie sync.Once 193 | // Create handlers 194 | // Send handler (sends file to caller) 195 | http.HandleFunc("/send/"+path, func(w http.ResponseWriter, r *http.Request) { 196 | if !cfg.KeepAlive && strings.HasPrefix(r.Header.Get("User-Agent"), "Mozilla") { 197 | if cookie.Value == "" { 198 | initCookie.Do(func() { 199 | value, err := util.GetSessionID() 200 | if err != nil { 201 | log.Println("Unable to generate session ID", err) 202 | app.stopChannel <- true 203 | return 204 | } 205 | cookie.Value = value 206 | http.SetCookie(w, &cookie) 207 | }) 208 | } else { 209 | // Check for the expected cookie and value 210 | // If it is missing or doesn't match 211 | // return a 400 status 212 | rcookie, err := r.Cookie(cookie.Name) 213 | if err != nil { 214 | http.Error(w, err.Error(), http.StatusBadRequest) 215 | return 216 | } 217 | if rcookie.Value != cookie.Value { 218 | http.Error(w, "mismatching cookie", http.StatusBadRequest) 219 | return 220 | } 221 | // If the cookie exits and matches 222 | // this is an aadditional request. 223 | // Increment the waitgroup 224 | waitgroup.Add(1) 225 | } 226 | // Remove connection from the waitgroup when done 227 | defer waitgroup.Done() 228 | } 229 | w.Header().Set("Content-Disposition", "attachment; filename=\""+ 230 | app.body.Filename+ 231 | "\"; filename*=UTF-8''"+ 232 | url.QueryEscape(app.body.Filename)) 233 | http.ServeFile(w, r, app.body.Path) 234 | }) 235 | // Upload handler (serves the upload page) 236 | http.HandleFunc("/receive/"+path, func(w http.ResponseWriter, r *http.Request) { 237 | htmlVariables := struct { 238 | Route string 239 | File string 240 | }{} 241 | htmlVariables.Route = "/receive/" + path 242 | switch r.Method { 243 | case "POST": 244 | filenames := util.ReadFilenames(app.outputDir) 245 | reader, err := r.MultipartReader() 246 | if err != nil { 247 | fmt.Fprintf(w, "Upload error: %v\n", err) 248 | log.Printf("Upload error: %v\n", err) 249 | app.stopChannel <- true 250 | return 251 | } 252 | transferredFiles := []string{} 253 | progressBar := pb.New64(r.ContentLength) 254 | progressBar.ShowCounters = false 255 | for { 256 | part, err := reader.NextPart() 257 | if err == io.EOF { 258 | break 259 | } 260 | // iIf part.FileName() is empty, skip this iteration. 261 | if part.FileName() == "" { 262 | continue 263 | } 264 | // Prepare the destination 265 | fileName := getFileName(filepath.Base(part.FileName()), filenames) 266 | out, err := os.Create(filepath.Join(app.outputDir, fileName)) 267 | if err != nil { 268 | // Output to server 269 | fmt.Fprintf(w, "Unable to create the file for writing: %s\n", err) 270 | // Output to console 271 | log.Printf("Unable to create the file for writing: %s\n", err) 272 | // Send signal to server to shutdown 273 | app.stopChannel <- true 274 | return 275 | } 276 | defer out.Close() 277 | // Add name of new file 278 | filenames = append(filenames, fileName) 279 | // Write the content from POSTed file to the out 280 | fmt.Println("Transferring file: ", out.Name()) 281 | progressBar.Prefix(out.Name()) 282 | progressBar.Start() 283 | buf := make([]byte, 1024) 284 | for { 285 | // Read a chunk 286 | n, err := part.Read(buf) 287 | if err != nil && err != io.EOF { 288 | // Output to server 289 | fmt.Fprintf(w, "Unable to write file to disk: %v", err) 290 | // Output to console 291 | fmt.Printf("Unable to write file to disk: %v", err) 292 | // Send signal to server to shutdown 293 | app.stopChannel <- true 294 | return 295 | } 296 | if n == 0 { 297 | break 298 | } 299 | // Write a chunk 300 | if _, err := out.Write(buf[:n]); err != nil { 301 | // Output to server 302 | fmt.Fprintf(w, "Unable to write file to disk: %v", err) 303 | // Output to console 304 | log.Printf("Unable to write file to disk: %v", err) 305 | // Send signal to server to shutdown 306 | app.stopChannel <- true 307 | return 308 | } 309 | progressBar.Add(n) 310 | } 311 | transferredFiles = append(transferredFiles, out.Name()) 312 | } 313 | progressBar.FinishPrint("File transfer completed") 314 | // Set the value of the variable to the actually transferred files 315 | htmlVariables.File = strings.Join(transferredFiles, ", ") 316 | serveTemplate("done", pages.Done, w, htmlVariables) 317 | if !cfg.KeepAlive { 318 | app.stopChannel <- true 319 | } 320 | case "GET": 321 | serveTemplate("upload", pages.Upload, w, htmlVariables) 322 | } 323 | }) 324 | // Wait for all wg to be done, then send shutdown signal 325 | go func() { 326 | waitgroup.Wait() 327 | if cfg.KeepAlive || !app.expectParallelRequests { 328 | return 329 | } 330 | app.stopChannel <- true 331 | }() 332 | go func() { 333 | netListener := tcpKeepAliveListener{listener.(*net.TCPListener)} 334 | if cfg.Secure { 335 | if err := httpserver.ServeTLS(netListener, cfg.TlsCert, cfg.TlsKey); err != http.ErrServerClosed { 336 | log.Fatalln("error starting the server:", err) 337 | } 338 | } else { 339 | if err := httpserver.Serve(netListener); err != http.ErrServerClosed { 340 | log.Fatalln("error starting the server", err) 341 | } 342 | } 343 | }() 344 | app.instance = httpserver 345 | return app, nil 346 | } 347 | 348 | // openBrowser navigates to a url using the default system browser 349 | func openBrowser(url string) { 350 | var err error 351 | switch runtime.GOOS { 352 | case "linux": 353 | err = exec.Command("xdg-open", url).Start() 354 | case "windows": 355 | err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() 356 | case "darwin": 357 | err = exec.Command("open", url).Start() 358 | default: 359 | err = fmt.Errorf("failed to open browser on platform: %s", runtime.GOOS) 360 | } 361 | if err != nil { 362 | log.Fatal(err) 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /server/tcpkeepalivelistener.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | // Copyright (c) 2009 The Go Authors. All rights reserved. 4 | 5 | // Redistribution and use in source and binary forms, with or without 6 | // modification, are permitted provided that the following conditions are 7 | // met: 8 | 9 | // * Redistributions of source code must retain the above copyright 10 | // notice, this list of conditions and the following disclaimer. 11 | // * Redistributions in binary form must reproduce the above 12 | // copyright notice, this list of conditions and the following disclaimer 13 | // in the documentation and/or other materials provided with the 14 | // distribution. 15 | // * Neither the name of Google Inc. nor the names of its 16 | // contributors may be used to endorse or promote products derived from 17 | // this software without specific prior written permission. 18 | 19 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | import ( 32 | "net" 33 | "time" 34 | ) 35 | 36 | // tcpKeepAliveListener applies TCP keepalives to the listener 37 | type tcpKeepAliveListener struct { 38 | *net.TCPListener 39 | } 40 | 41 | // Accept accepts TCP 42 | func (ln tcpKeepAliveListener) Accept() (net.Conn, error) { 43 | tc, err := ln.AcceptTCP() 44 | if err != nil { 45 | return nil, err 46 | } 47 | if err := tc.SetKeepAlive(true); err != nil { 48 | panic(err) 49 | } 50 | if err := tc.SetKeepAlivePeriod(3 * time.Minute); err != nil { 51 | panic(err) 52 | } 53 | return tc, nil 54 | } 55 | -------------------------------------------------------------------------------- /server/util.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | "io" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | func serveTemplate(name string, tmpl string, w io.Writer, data interface{}) { 12 | t, err := template.New(name).Parse(tmpl) 13 | if err != nil { 14 | panic(err) 15 | } 16 | if err := t.Execute(w, data); err != nil { 17 | panic(err) 18 | } 19 | } 20 | 21 | // getFileName generates a file name based on the existing files in the directory 22 | // if name isn't taken leave it unchanged 23 | // else change name to format "name(number).ext" 24 | func getFileName(newFilename string, fileNamesInTargetDir []string) string { 25 | fileExt := filepath.Ext(newFilename) 26 | fileName := strings.TrimSuffix(newFilename, fileExt) 27 | number := 1 28 | i := 0 29 | for i < len(fileNamesInTargetDir) { 30 | if newFilename == fileNamesInTargetDir[i] { 31 | newFilename = fmt.Sprintf("%s(%v)%s", fileName, number, fileExt) 32 | number++ 33 | i = 0 34 | } 35 | i++ 36 | } 37 | return newFilename 38 | } 39 | -------------------------------------------------------------------------------- /util/net.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "net" 5 | "regexp" 6 | 7 | externalip "github.com/glendc/go-external-ip" 8 | ) 9 | 10 | // Interfaces returns a `name:ip` map of the suitable interfaces found 11 | func Interfaces(listAll bool) (map[string]string, error) { 12 | names := make(map[string]string) 13 | ifaces, err := net.Interfaces() 14 | if err != nil { 15 | return names, err 16 | } 17 | var re = regexp.MustCompile(`^(veth|br\-|docker|lo|EHC|XHC|bridge|gif|stf|p2p|awdl|utun|tun|tap)`) 18 | for _, iface := range ifaces { 19 | if !listAll && re.MatchString(iface.Name) { 20 | continue 21 | } 22 | if iface.Flags&net.FlagUp == 0 { 23 | continue 24 | } 25 | ip, err := FindIP(iface) 26 | if err != nil { 27 | continue 28 | } 29 | names[iface.Name] = ip 30 | } 31 | return names, nil 32 | } 33 | 34 | // GetExternalIP of this host 35 | func GetExternalIP() (net.IP, error) { 36 | consensus := externalip.DefaultConsensus(nil, nil) 37 | // Get your IP, which is never when err is 38 | ip, err := consensus.ExternalIP() 39 | if err != nil { 40 | return nil, err 41 | } 42 | return ip, nil 43 | } 44 | -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | "errors" 7 | "io" 8 | "net" 9 | "os" 10 | "os/user" 11 | "path/filepath" 12 | "runtime" 13 | "strconv" 14 | "strings" 15 | "time" 16 | 17 | "github.com/jhoonb/archivex" 18 | ) 19 | 20 | // Expand tilde in paths 21 | func Expand(input string) string { 22 | if runtime.GOOS == "windows" { 23 | return input 24 | } 25 | usr, _ := user.Current() 26 | dir := usr.HomeDir 27 | if input == "~" { 28 | input = dir 29 | } else if strings.HasPrefix(input, "~/") { 30 | input = filepath.Join(dir, input[2:]) 31 | } 32 | return input 33 | } 34 | 35 | // ZipFiles and return the resulting zip's filename 36 | func ZipFiles(files []string) (string, error) { 37 | zip := new(archivex.ZipFile) 38 | tmpfile, err := os.CreateTemp("", "qrcp") 39 | if err != nil { 40 | return "", err 41 | } 42 | tmpfile.Close() 43 | if err := os.Rename(tmpfile.Name(), tmpfile.Name()+".zip"); err != nil { 44 | return "", err 45 | } 46 | tmpfile, err = os.OpenFile(tmpfile.Name()+".zip", os.O_RDWR, 0644) 47 | if err != nil { 48 | return "", err 49 | } 50 | if err := zip.CreateWriter(tmpfile.Name(), tmpfile); err != nil { 51 | return "", err 52 | } 53 | 54 | for _, filename := range files { 55 | fileinfo, err := os.Stat(filename) 56 | if err != nil { 57 | return "", err 58 | } 59 | if fileinfo.IsDir() { 60 | if err := zip.AddAll(filename, true); err != nil { 61 | return "", err 62 | } 63 | } else { 64 | file, err := os.Open(filename) 65 | if err != nil { 66 | return "", err 67 | } 68 | defer file.Close() 69 | if err := zip.Add(filename, file, fileinfo); err != nil { 70 | return "", err 71 | } 72 | } 73 | } 74 | 75 | if err := zip.Writer.Close(); err != nil { 76 | return "", err 77 | } 78 | if err := tmpfile.Close(); err != nil { 79 | return "", err 80 | } 81 | 82 | return zip.Name, nil 83 | } 84 | 85 | // GetRandomURLPath returns a random string of 4 alphanumeric characters 86 | func GetRandomURLPath() string { 87 | timeNum := time.Now().UTC().UnixNano() 88 | alphaString := strconv.FormatInt(timeNum, 36) 89 | return alphaString[len(alphaString)-4:] 90 | } 91 | 92 | // GetSessionID returns a base64 encoded string of 40 random characters 93 | func GetSessionID() (string, error) { 94 | randbytes := make([]byte, 40) 95 | if _, err := io.ReadFull(rand.Reader, randbytes); err != nil { 96 | return "", err 97 | } 98 | return base64.StdEncoding.EncodeToString(randbytes), nil 99 | } 100 | 101 | // GetInterfaceAddress returns the address of the network interface to 102 | // bind the server to. If the interface is "any", it will return 0.0.0.0. 103 | // If no interface is found with that name, an error is returned 104 | func GetInterfaceAddress(ifaceString string) (string, error) { 105 | if ifaceString == "any" { 106 | return "0.0.0.0", nil 107 | } 108 | ifaces, err := net.Interfaces() 109 | if err != nil { 110 | return "", err 111 | } 112 | var candidateInterface *net.Interface 113 | for _, iface := range ifaces { 114 | if iface.Name == ifaceString { 115 | candidateInterface = &iface 116 | break 117 | } 118 | } 119 | if candidateInterface != nil { 120 | ip, err := FindIP(*candidateInterface) 121 | if err != nil { 122 | return "", err 123 | } 124 | return ip, nil 125 | } 126 | return "", errors.New("unable to find interface") 127 | } 128 | 129 | // FindIP returns the IP address of the passed interface, and an error 130 | func FindIP(iface net.Interface) (string, error) { 131 | var ip string 132 | addrs, err := iface.Addrs() 133 | if err != nil { 134 | return "", err 135 | } 136 | for _, addr := range addrs { 137 | if ipnet, ok := addr.(*net.IPNet); ok { 138 | if ipnet.IP.IsLinkLocalUnicast() { 139 | continue 140 | } 141 | if ipnet.IP.To4() != nil { 142 | ip = ipnet.IP.String() 143 | continue 144 | } 145 | // Use IPv6 only if an IPv4 hasn't been found yet. 146 | // This is eventually overwritten with an IPv4, if found (see above) 147 | if ip == "" { 148 | ip = "[" + ipnet.IP.String() + "]" 149 | } 150 | } 151 | } 152 | if ip == "" { 153 | return "", errors.New("unable to find an IP for this interface") 154 | } 155 | return ip, nil 156 | } 157 | 158 | // ReadFilenames from dir 159 | func ReadFilenames(dir string) []string { 160 | files, err := os.ReadDir(dir) 161 | if err != nil { 162 | panic(err) 163 | } 164 | // Create array of names of files which are stored in dir 165 | // used later to set valid name for received files 166 | filenames := make([]string, 0, len(files)) 167 | for _, fi := range files { 168 | filenames = append(filenames, fi.Name()) 169 | } 170 | return filenames 171 | } 172 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import "fmt" 4 | 5 | var ( 6 | app = "qrcp" 7 | version = "dev" 8 | date = "n/a" 9 | ) 10 | 11 | // String returns a string representation of the build. 12 | func String() string { 13 | return fmt.Sprintf("%s %s [date: %s]", app, version, date) 14 | } 15 | --------------------------------------------------------------------------------