Repository: claudiodangelis/qrcp Branch: main Commit: 3b176183de83 Files: 44 Total size: 341.8 KB Directory structure: gitextract_1sje0g3l/ ├── .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 │ ├── 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 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge 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. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities 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. 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. ## Scope 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. ## Enforcement 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. 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. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ ================================================ FILE: .github/CONTRIBUTING.md ================================================ Contributions to this project are super welcome, so here's my recommendations: - Make sure that the no one is already working on what you are going to fix/implement 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 - If you want to take on an open issue, please announce it in the thread 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 - Discuss implementation before writing the actual code 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 - Review pending pull requests Help other users by reviewing their code - Explain the pull requests Help reviewers by explaining how the patch works, what bugs/problems it addresses and _how_ it should be tested - Address one problem per pull request When possible, avoid submitting a pull request that addresses more than problem - Run `go fmt` before submitting the pull request and address `golint` issues ================================================ FILE: .github/FUNDING.yml ================================================ github: [claudiodangelis] custom: ["https://www.paypal.me/claudiodangelis", "https://www.buymeacoffee.com/claudiodangelis"] ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ I'm opening this issue because: - [ ] I have found a bug - [ ] I want to request a feature - [ ] I have a question - [ ] Other - My Go version is: _(paste the output of `go version` and remember that qrcp requires at least version 1.8 of Go)_ - My [GOPATH](https://github.com/golang/go/wiki/GOPATH) is set to: _(paste the output of `echo $GOPATH`)_ ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: gomod directory: / schedule: interval: daily ================================================ FILE: .github/workflows/main.yml ================================================ name: CI on: push: tags: [ 'v*' ] branches: [ main ] pull_request: permissions: contents: write env: GOLANG_VERSION: 1.21.x jobs: lint: name: lint runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: ${{ env.GOLANG_VERSION }} - name: golangci-lint uses: golangci/golangci-lint-action@v7 with: version: v2.0 test: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v5 with: go-version: ${{ env.GOLANG_VERSION }} - name: Install dependencies run: go get . - name: Build run: go build -v ./... - name: Test with the Go CLI run: go test ./... e2e: needs: [ test, lint ] runs-on: ${{ matrix.os }} strategy: matrix: os: [ ubuntu-24.04, macos-15, windows-latest ] steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: ${{ env.GOLANG_VERSION }} - name: Run tests on *nix if: runner.os != 'Windows' run: | TEST_CONTENT="hello" TEST_FILE="/tmp/qrcp-test.txt" echo $TEST_CONTENT > $TEST_FILE go build OS=$(uname) INTERFACE=lo if [[ "$OS" == "Darwin" ]]; then INTERFACE="lo0" fi ./qrcp -i $INTERFACE -p 1606 --path test $TEST_FILE > /dev/null 2>&1 & QRCP_PID=$! sleep 2 CURL_OUTPUT=$(curl -s http://127.0.0.1:1606/send/test) kill $QRCP_PID || true if [[ "${TEST_CONTENT}" != "${CURL_OUTPUT}" ]]; then exit 1 fi - name: Run tests on Windows if: runner.os == 'Windows' run: | $TestContent = "hello" $TestFile = "$env:TEMP\qrcp-test.txt" $TestContent | Out-File -FilePath $TestFile -Encoding UTF8 go build $Job = Start-Job -ScriptBlock { Start-Process -FilePath ./qrcp -ArgumentList "-i", "any", "-p", "1606", "--path", "test", "$env:TEMP\qrcp-test.txt" -NoNewWindow -Wait } Start-Sleep -Seconds 2 $Request = Invoke-WebRequest -Uri http://127.0.0.1:1606/send/test $FileContent = Get-Content -Path $TestFile -Raw if ($Request.Content -ne $FileContent) { Write-Host "Expected: $FileContent" Write-Host "Got: $($Request.Content)" exit 1 } release: runs-on: ubuntu-24.04 needs: [ e2e ] if: startsWith(github.event.ref, 'refs/tags/') steps: - uses: actions/checkout@v4 - name: Unshallow run: git fetch --prune --unshallow - name: Set up Go uses: actions/setup-go@v5 with: go-version: ${{ env.GOLANG_VERSION }} - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 with: args: release --clean version: '~> v2' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .gitignore ================================================ qr-filetransfer qrcp dist ================================================ FILE: .goreleaser.yml ================================================ # yaml-language-server: $schema=https://goreleaser.com/static/schema.json before: hooks: - go mod download builds: - env: - CGO_ENABLED=0 ldflags: - -s -w -X github.com/claudiodangelis/qrcp/version.version={{.Version}} -X github.com/claudiodangelis/qrcp/version.date={{.Date}} goos: - linux - darwin - windows goarch: - 386 - amd64 - arm - arm64 goarm: - 7 ignore: - goos: darwin goarch: 386 archives: - format_overrides: - goos: windows formats: [ tar.gz, zip ] checksum: name_template: 'checksums.txt' changelog: sort: asc filters: exclude: - '^docs:' - '^test:' - '^chore:' release: footer: | ## Downloads | Platform | Download link | |----------|------------------------------------------------------------------------------------------------------------------------------| | Linux | [deb package](https://github.com/claudiodangelis/qrcp/releases/download/{{ .Tag }}/qrcp_{{ .Version }}_linux_amd64.deb) | | Linux | [RPM package](https://github.com/claudiodangelis/qrcp/releases/download/{{ .Tag }}/qrcp_{{ .Version }}_linux_amd64.rpm) | | macOS | [macOS package](https://github.com/claudiodangelis/qrcp/releases/download/{{ .Tag }}/qrcp_{{ .Version }}_darwin_amd64.tar.gz) | | Windows | [Windows package](https://github.com/claudiodangelis/qrcp/releases/download/{{ .Tag }}/qrcp_{{ .Version }}_windows_amd64.tar.gz) | Refer to the list of assets below for all supported platform. nfpms: - homepage: https://qrcp.sh maintainer: Claudio d'Angelis description: Transfer files over wifi from your computer to your mobile device by scanning a QR code without leaving the terminal. license: MIT formats: - deb - rpm ================================================ FILE: DEVELOPMENT.md ================================================ # Development ## Versioning `qrcp` uses [semver](https://semver.org) for releases. Version number is defined in `cmd/version.go`. ## Releases 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`. The relevant files are: - .goreleases.yml - .github/workflows/main.yml The release action is triggered when a tag is pushed to the master branch. ## Development workflow 1. Open a PR 2. Let someone review it 3. Squash commits and merge to master 4. When ready to release, add a tag 5. Wait for Github Action to process the release ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018 Claudio d'Angelis Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ ![Logo](docs/img/logo.svg) # $ qrcp Transfer files over Wi-Fi from your computer to a mobile device by scanning a QR code without leaving the terminal. [![Go Report Card](https://goreportcard.com/badge/github.com/claudiodangelis/qrcp)](https://goreportcard.com/report/github.com/claudiodangelis/qrcp) 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. --- ## How does it work? ![Screenshot](docs/img/screenshot.png) `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. The tool prints a QR code that encodes the text: ``` http://{address}:{port}/{random_path} ``` 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. ### Demo **Send files to mobile:** ![screenshot](docs/img/demo.gif) **Receive files from mobile:** ![Screenshot](docs/img/mobile-demo.gif) --- ## Installation ### Using Go (Latest Development Version) Requires Go 1.18 or later: ```sh go install github.com/claudiodangelis/qrcp@latest ``` ### Prebuilt Binaries Download the latest release for your platform from the [Releases](https://github.com/claudiodangelis/qrcp/releases) page. | Platform | Instructions | |-------------|------------------------------------------------------------------------------------------| | **Linux** | Extract the `.tar.gz` archive, move the binary to `/usr/local/bin`, and set permissions. | | **Windows** | Extract the `.tar.gz` archive and place the `.exe` file in a directory in your `PATH`. | | **macOS** | Extract the `.tar.gz` archive, move the binary to `/usr/local/bin`, and set permissions. | ### Package Managers | Platform | Package Manager | Command | |-------------|-----------------|------------------------------------------------| | **Linux** | ArchLinux (AUR) | `yay -S qrcp-bin` or `yay -S qrcp` | | **Linux** | Debian/Ubuntu | `sudo dpkg -i qrcp__linux_x86_64.deb` | | **Linux** | CentOS/Fedora | `sudo rpm -i qrcp__linux_x86_64.rpm` | | **Windows** | WinGet | `winget install --id=claudiodangelis.qrcp -e` | | **Windows** | Scoop | `scoop install qrcp` | | **Windows** | Chocolatey | `choco install qrcp` | | **macOS** | Homebrew | `brew install qrcp` | ### Confirm Installation After installation, verify that `qrcp` is working: ```sh qrcp --help ``` --- ## Usage ### Send Files | Action | Command Example | |-----------------------------|-----------------------------------| | **Send a file** | `qrcp MyDocument.pdf` | | **Send multiple files** | `qrcp MyDocument.pdf IMG0001.jpg` | | **Send a folder** | `qrcp Documents/` | | **Zip before transferring** | `qrcp --zip LongVideo.avi` | ### Receive Files | Action | Command Example | |-------------------------------------|----------------------------------| | **Receive to current directory** | `qrcp receive` | | **Receive to a specific directory** | `qrcp receive --output=/tmp/dir` | --- ## Configuration `qrcp` works without prior configuration, but you can customize it using a configuration file or environment variables. ### Configuration File The default configuration file is stored in `$XDG_CONFIG_HOME/qrcp/config.yml`. You can specify a custom location using the `--config` flag: ```sh qrcp --config /tmp/qrcp.yml MyDocument.pdf ``` ### Configuration Options | Key | Type | Description | |-------------|---------|--------------------------------------------------------------------------------| | `interface` | String | Network interface to bind the web server to. Use `any` to bind to `0.0.0.0`. | | `bind` | String | Address to bind the web server to. Overrides `interface`. | | `port` | Integer | Port to use. Defaults to a random port. | | `path` | String | Path to use in the URL. Defaults to a random string. | | `output` | String | Default directory to receive files. Defaults to the current working directory. | | `fqdn` | String | Fully qualified domain name to use in the URL instead of the IP address. | | `keep-alive` | Bool | Keep the server alive after transferring files. Defaults to `false`. | | `secure` | Bool | Use HTTPS instead of HTTP. Defaults to `false`. | | `tls-cert` | String | Path to the TLS certificate. Used only when `secure: true`. | | `tls-key` | String | Path to the TLS key. Used only when `secure: true`. | ### Environment Variables All configuration parameters can also be set via environment variables prefixed with `QRCP_`: - `$QRCP_INTERFACE` - `$QRCP_PORT` - `$QRCP_KEEPALIVE` --- ## Advanced Usage ### Network Interface To use a specific network interface: ```sh qrcp -i tun0 MyDocument.pdf ``` To bind the web server to all interfaces: ```sh qrcp -i any MyDocument.pdf ``` ### HTTPS Enable secure transfers with HTTPS by providing a TLS certificate and key: ```sh qrcp --tls-cert /path/to/cert.pem --tls-key /path/to/cert.key MyDocument.pdf ``` --- ## Shell Completion `qrcp` provides shell completion scripts for Bash, Zsh, and Fish. | Shell | Command Example | |----------|---------------------------------------------| | **Bash** | `source <(qrcp completion bash)` | | **Zsh** | `qrcp completion zsh > "${fpath[1]}/_qrcp"` | | **Fish** | `qrcp completion fish | source` | --- ## Authors and Credits - **Author**: [Claudio d'Angelis](https://t.me/claudiodangelis) - **Logo**: Provided by [@arasatasaygin](https://github.com/arasatasaygin) as part of the [openlogos](https://github.com/arasatasaygin/openlogos) initiative. - **Releases**: Managed with [goreleaser](https://goreleaser.com). --- ## Clones and Similar Projects | Project Name | Description | |----------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------| | [qr-fileshare](https://github.com/shivensinha4/qr-fileshare) | A similar idea executed in NodeJS with a React interface. | | [instant-file-transfer](https://github.com/maximumdata/instant-file-transfer) _(Uncredited)_ | Node.js project similar to this. | | [qr-filetransfer](https://github.com/sdushantha/qr-filetransfer) | Python clone of this project. | | [qr-filetransfer](https://github.com/svenkatreddy/qr-filetransfer) | Another Node.js clone of this project. | | [qr-transfer-node](https://github.com/codezoned/qr-transfer-node) | Another Node.js clone of this project. | | [QRDELIVER](https://github.com/realdennis/qrdeliver) | Node.js project similar to this. | | [qrfile](https://github.com/sgbj/qrfile) | Transfer files by scanning a QR code. | | [quick-transfer](https://github.com/CodeMan99/quick-transfer) | Node.js clone of this project. | | [share-file-qr](https://github.com/pwalch/share-file-qr) | Python re-implementation of this project. | | [share-files](https://github.com/antoaravinth/share-files) _(Uncredited)_ | Yet another Node.js clone of this project. | | [ezshare](https://github.com/mifi/ezshare) | Another Node.js two-way file sharing tool supporting folders and multiple files. | | [local_file_share](https://github.com/woshimanong1990/local_file_share) | _"Share local file to other people, OR smartphone download files which is in PC."_ | | [qrcp](https://github.com/pearl2201/qrcp) | A desktop app clone of `qrcp`, written with C# and .NET Core, works for Windows. | | [swift_file](https://github.com/mateoradman/swift_file) | Rust project inspired by `qrcp`. | | [qrcp-android](https://github.com/ianfixes/qrcp-android) | Android app inspired by `qrcp`. | --- ## License MIT. See [LICENSE](LICENSE). ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Supported Versions | Version | Supported | | ------- | ------------------ | | > 0.8.0 | :white_check_mark: | | < 0.8.0 | :x: | ## Reporting a Vulnerability If you have found a security issue, please immediately contact the main maintainer of the project at claudiodangelis@gmail.com. ================================================ FILE: application/application.go ================================================ package application type Flags struct { Quiet bool KeepAlive bool ListAllInterfaces bool Port int Path string Interface string Bind string FQDN string Zip bool Config string Browser bool Secure bool TlsCert string TlsKey string Output string Reversed bool } type App struct { Flags Flags Name string } func New() App { return App{ Name: "qrcp", Flags: Flags{}, } } ================================================ FILE: body/payload.go ================================================ package body import ( "os" "path/filepath" "github.com/claudiodangelis/qrcp/util" ) // Body to transfer type Body struct { Filename string Path string DeleteAfterTransfer bool } // Delete the payload from disk func (p Body) Delete() error { return os.RemoveAll(p.Path) } // FromArgs returns a payload from args func FromArgs(args []string, zipFlag bool) (Body, error) { shouldzip := len(args) > 1 || zipFlag var files []string // Check if content exists for _, arg := range args { file, err := os.Stat(arg) if err != nil { return Body{}, err } // If at least one argument is dir, the content will be zipped if file.IsDir() { shouldzip = true } files = append(files, arg) } // Prepare the content // TODO: Research cleaner code var content string if shouldzip { zip, err := util.ZipFiles(files) if err != nil { return Body{}, err } content = zip } else { content = args[0] } return Body{ Path: content, Filename: filepath.Base(content), DeleteAfterTransfer: shouldzip, }, nil } ================================================ FILE: cmd/completion.go ================================================ package cmd import ( "os" "github.com/spf13/cobra" ) // completionCmd represents the completion command var completionCmd = &cobra.Command{ Use: "completion [bash|zsh|fish|powershell]", Short: "Generate completion script", Long: `To load completions: Bash: $ source <(qrcp completion bash) # To load completions for each session, execute once: Linux: $ qrcp completion bash > /etc/bash_completion.d/qrcp MacOS: $ qrcp completion bash > /usr/local/etc/bash_completion.d/qrcp Zsh: # If shell completion is not already enabled in your environment you will need # to enable it. You can execute the following once: $ echo "autoload -U compinit; compinit" >> ~/.zshrc # To load completions for each session, execute once: $ qrcp completion zsh > "${fpath[1]}/_qrcp" # You will need to start a new shell for this setup to take effect. Fish: $ qrcp completion fish | source # To load completions for each session, execute once: $ qrcp completion fish > ~/.config/fish/completions/qrcp.fish `, DisableFlagsInUseLine: true, ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), Run: func(cmd *cobra.Command, args []string) { switch args[0] { case "bash": if err := cmd.Root().GenBashCompletion(os.Stdout); err != nil { panic(err) } case "zsh": if err := cmd.Root().GenZshCompletion(os.Stdout); err != nil { panic(err) } case "fish": if err := cmd.Root().GenFishCompletion(os.Stdout, true); err != nil { panic(err) } case "powershell": if err := cmd.Root().GenPowerShellCompletion(os.Stdout); err != nil { panic(err) } } }, } ================================================ FILE: cmd/config.go ================================================ package cmd import ( "fmt" "github.com/claudiodangelis/qrcp/config" "github.com/spf13/cobra" ) func configCmdFunc(command *cobra.Command, args []string) error { return config.Wizard(app) } var configCmd = &cobra.Command{ Use: "config", Short: "Configure qrcp", 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.", Aliases: []string{"c", "cfg"}, RunE: configCmdFunc, } var migrateCmd = &cobra.Command{ Use: "migrate", Short: "Migrate the legacy configuration file", Long: "Migrate the legacy JSON configuration file to the new YAML format", Run: func(cmd *cobra.Command, args []string) { ok, err := config.Migrate(app) if err != nil { fmt.Println("error while migrating the legacy JSON configuration file:", err) } if ok { fmt.Println("Legacy JSON configuration file has been successfully deleted") } }, } ================================================ FILE: cmd/qrcp.go ================================================ package cmd import ( "github.com/claudiodangelis/qrcp/application" "github.com/spf13/cobra" ) var app application.App func init() { app = application.New() rootCmd.AddCommand(sendCmd) rootCmd.AddCommand(receiveCmd) rootCmd.AddCommand(configCmd) rootCmd.AddCommand(versionCmd) rootCmd.AddCommand(completionCmd) configCmd.AddCommand(migrateCmd) // Global command flags rootCmd.PersistentFlags().BoolVarP(&app.Flags.Quiet, "quiet", "q", false, "only print errors") rootCmd.PersistentFlags().BoolVarP(&app.Flags.KeepAlive, "keep-alive", "k", false, "keep server alive after transferring") rootCmd.PersistentFlags().BoolVarP(&app.Flags.ListAllInterfaces, "list-all-interfaces", "l", false, "list all available interfaces when choosing the one to use") rootCmd.PersistentFlags().IntVarP(&app.Flags.Port, "port", "p", 0, "port to use for the server") rootCmd.PersistentFlags().StringVar(&app.Flags.Path, "path", "", "path to use. Defaults to a random string") rootCmd.PersistentFlags().StringVarP(&app.Flags.Interface, "interface", "i", "", "network interface to use for the server") rootCmd.PersistentFlags().StringVar(&app.Flags.Bind, "bind", "", "address to bind the web server to") rootCmd.PersistentFlags().StringVarP(&app.Flags.FQDN, "fqdn", "d", "", "fully-qualified domain name to use for the resulting URLs") rootCmd.PersistentFlags().BoolVarP(&app.Flags.Zip, "zip", "z", false, "zip content before transferring") rootCmd.PersistentFlags().StringVarP(&app.Flags.Config, "config", "c", "", "path to the config file, defaults to $XDG_CONFIG_HOME/qrcp/config.json") rootCmd.PersistentFlags().BoolVarP(&app.Flags.Browser, "browser", "b", false, "display the QR code in a browser window") rootCmd.PersistentFlags().BoolVarP(&app.Flags.Secure, "secure", "s", false, "use https connection") rootCmd.PersistentFlags().StringVar(&app.Flags.TlsCert, "tls-cert", "", "path to TLS certificate to use with HTTPS") rootCmd.PersistentFlags().StringVar(&app.Flags.TlsKey, "tls-key", "", "path to TLS private key to use with HTTPS") rootCmd.PersistentFlags().BoolVarP(&app.Flags.Reversed, "reversed", "r", false, "Reverse QR code (black text on white background)") // Receive command flags receiveCmd.PersistentFlags().StringVarP(&app.Flags.Output, "output", "o", "", "output directory for receiving files") } // The root command (`qrcp`) is like a shortcut of the `send` command var rootCmd = &cobra.Command{ Use: "qrcp", Args: cobra.MinimumNArgs(1), RunE: sendCmdFunc, SilenceErrors: true, SilenceUsage: true, } // Execute the root command func Execute() error { if err := rootCmd.Execute(); err != nil { rootCmd.PrintErrf("Error: %v\nRun `qrcp help` for help.\n", err) return err } return nil } ================================================ FILE: cmd/receive.go ================================================ package cmd import ( "fmt" "github.com/claudiodangelis/qrcp/config" "github.com/claudiodangelis/qrcp/logger" "github.com/claudiodangelis/qrcp/qr" "github.com/claudiodangelis/qrcp/server" "github.com/eiannone/keyboard" "github.com/spf13/cobra" ) func receiveCmdFunc(command *cobra.Command, args []string) error { log := logger.New(app.Flags.Quiet) // Load configuration cfg := config.New(app) // Create the server srv, err := server.New(&cfg) if err != nil { return err } // Sets the output directory if err := srv.ReceiveTo(cfg.Output); err != nil { return err } // Prints the URL to scan to screen log.Print(`Scan the following URL with a QR reader to start the file transfer, press CTRL+C or "q" to exit:`) log.Print(srv.ReceiveURL) // Renders the QR qr.RenderString(srv.ReceiveURL, cfg.Reversed) if app.Flags.Browser { srv.DisplayQR(srv.ReceiveURL) } if err := keyboard.Open(); err == nil { defer func() { keyboard.Close() }() go func() { for { char, key, _ := keyboard.GetKey() if string(char) == "q" || key == keyboard.KeyCtrlC { srv.Shutdown() } } }() } else { log.Print(fmt.Sprintf("Warning: keyboard not detected: %v", err)) } if err := srv.Wait(); err != nil { return err } return nil } var receiveCmd = &cobra.Command{ Use: "receive", Aliases: []string{"r"}, Short: "Receive one or more files", 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.", Example: `# Receive files in the current directory qrcp receive # Receive files in a specific directory qrcp receive --output /tmp `, RunE: receiveCmdFunc, } ================================================ FILE: cmd/send.go ================================================ package cmd import ( "fmt" "github.com/claudiodangelis/qrcp/body" "github.com/claudiodangelis/qrcp/config" "github.com/claudiodangelis/qrcp/logger" "github.com/claudiodangelis/qrcp/qr" "github.com/eiannone/keyboard" "github.com/claudiodangelis/qrcp/server" "github.com/spf13/cobra" ) func sendCmdFunc(command *cobra.Command, args []string) error { log := logger.New(app.Flags.Quiet) body, err := body.FromArgs(args, app.Flags.Zip) if err != nil { return err } cfg := config.New(app) srv, err := server.New(&cfg) if err != nil { return err } // Sets the body srv.Send(body) log.Print(`Scan the following URL with a QR reader to start the file transfer, press CTRL+C or "q" to exit:`) log.Print(srv.SendURL) qr.RenderString(srv.SendURL, cfg.Reversed) if app.Flags.Browser { srv.DisplayQR(srv.SendURL) } if err := keyboard.Open(); err == nil { defer func() { keyboard.Close() }() go func() { for { char, key, _ := keyboard.GetKey() if string(char) == "q" || key == keyboard.KeyCtrlC { srv.Shutdown() } } }() } else { log.Print(fmt.Sprintf("Warning: keyboard not detected: %v", err)) } if err := srv.Wait(); err != nil { return err } return nil } var sendCmd = &cobra.Command{ Use: "send", Short: "Send a file(s) or directories from this host", Long: "Send a file(s) or directories from this host", Aliases: []string{"s"}, Example: `# Send /path/file.gif. Webserver listens on a random port qrcp send /path/file.gif # Shorter version: qrcp /path/file.gif # Zip file1.gif and file2.gif, then send the zip package qrcp /path/file1.gif /path/file2.gif # Zip the content of directory, then send the zip package qrcp /path/directory # Send file.gif by creating a webserver on port 8080 qrcp --port 8080 /path/file.gif `, Args: cobra.MinimumNArgs(1), RunE: sendCmdFunc, } ================================================ FILE: cmd/version.go ================================================ package cmd import ( "fmt" "github.com/claudiodangelis/qrcp/version" "github.com/spf13/cobra" ) var versionCmd = &cobra.Command{ Use: "version", Short: "Print version number and build information.", Run: func(c *cobra.Command, args []string) { fmt.Println(version.String()) }, } ================================================ FILE: config/config.go ================================================ package config import ( "errors" "fmt" "os" "path/filepath" "strconv" "strings" "github.com/adrg/xdg" "github.com/asaskevich/govalidator" "github.com/claudiodangelis/qrcp/application" "github.com/claudiodangelis/qrcp/logger" "github.com/claudiodangelis/qrcp/util" "github.com/manifoldco/promptui" "github.com/spf13/viper" ) type Config struct { Interface string `yaml:",omitempty"` Port int `yaml:",omitempty"` Bind string `yaml:",omitempty"` KeepAlive bool `yaml:",omitempty"` Path string `yaml:",omitempty"` Secure bool `yaml:",omitempty"` TlsKey string `yaml:",omitempty"` TlsCert string `yaml:",omitempty"` FQDN string `yaml:",omitempty"` Output string `yaml:",omitempty"` Reversed bool `yaml:",omitempty"` } var interactive bool = false func New(app application.App) Config { log := logger.New(app.Flags.Quiet) v := getViperInstance(app) var err error cfg := Config{} _, err = os.Stat(v.ConfigFileUsed()) if os.IsNotExist(err) { if err := os.MkdirAll(filepath.Dir(v.ConfigFileUsed()), os.ModeDir|os.ModePerm); err != nil { panic(err) } file, err := os.Create(v.ConfigFileUsed()) if err != nil { panic(err) } defer file.Close() } if err := v.ReadInConfig(); err != nil { panic(fmt.Errorf("fatal error config file: %s", err)) } // Load file cfg.Interface = v.GetString("interface") cfg.Bind = v.GetString("bind") cfg.Port = v.GetInt("port") cfg.KeepAlive = v.GetBool("keepAlive") cfg.Path = v.GetString("path") cfg.Secure = v.GetBool("secure") cfg.TlsKey = v.GetString("tls-key") cfg.TlsCert = v.GetString("tls-cert") cfg.FQDN = v.GetString("fqdn") cfg.Output = v.GetString("output") cfg.Reversed = v.GetBool("reversed") // Override if app.Flags.Interface != "" { cfg.Interface = app.Flags.Interface } if app.Flags.Bind != "" { cfg.Bind = app.Flags.Bind } if app.Flags.Port != 0 { cfg.Port = app.Flags.Port } if app.Flags.KeepAlive { cfg.KeepAlive = true } if app.Flags.Path != "" { cfg.Path = app.Flags.Path } if app.Flags.Secure { cfg.Secure = true } if app.Flags.TlsKey != "" { cfg.TlsKey = app.Flags.TlsKey } if app.Flags.TlsCert != "" { cfg.TlsCert = app.Flags.TlsCert } if app.Flags.FQDN != "" { cfg.FQDN = app.Flags.FQDN } if app.Flags.Output != "" { cfg.Output = app.Flags.Output } if app.Flags.Reversed { cfg.Reversed = true } // Discover interface if it's not been set yet if !interactive { if cfg.Interface == "" { cfg.Interface, err = chooseInterface(app.Flags) if err != nil { panic(err) } v.Set("interface", cfg.Interface) if err := v.WriteConfig(); err != nil { log.Print(fmt.Sprintf("Warning: the configuration file could not be saved: %v\n", err)) } } } return cfg } func getViperInstance(app application.App) *viper.Viper { var configType string var configFile string v := viper.New() if app.Flags.Config != "" { configFile = app.Flags.Config configType = filepath.Ext(configFile)[1:] } else { oldConfigFile := filepath.Join(xdg.ConfigHome, "qrcp", "config.json") // Check if old configuration file exists if _, err := os.Stat(oldConfigFile); os.IsNotExist(err) { configType = "yml" } else { configType = "json" } configFile = filepath.Join(xdg.ConfigHome, app.Name, fmt.Sprintf("config.%s", configType)) } v.SetConfigType(configType) v.SetConfigFile(configFile) v.AutomaticEnv() v.SetEnvPrefix(app.Name) v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) return v } func Wizard(app application.App) error { interactive = true cfg := New(app) v := getViperInstance(app) // Choose interface var err error cfg.Interface, err = chooseInterface(app.Flags) if err != nil { panic(err) } v.Set("interface", cfg.Interface) if err := v.WriteConfig(); err != nil { return err } // Ask for bind address validateBind := func(input string) error { if input == "" { return nil } if !govalidator.IsIPv4(input) { return errors.New("invalid address") } return nil } promptBind := promptui.Prompt{ Validate: validateBind, Label: "Enter bind address (this will override the chosen interface address)", Default: cfg.Bind, } if promptBindResultString, err := promptBind.Run(); err == nil { if promptBindResultString != "" { v.Set("bind", promptBindResultString) } } // Ask for port validatePort := func(input string) error { _, err := strconv.ParseUint(input, 10, 16) if err != nil { return errors.New("invalid number") } return nil } promptPort := promptui.Prompt{ Validate: validatePort, Label: "Choose port, 0 means random port", Default: fmt.Sprintf("%d", cfg.Port), } if promptPortResultString, err := promptPort.Run(); err == nil { if port, err := strconv.ParseUint(promptPortResultString, 10, 16); err == nil { if port > 0 { v.Set("port", port) } } } // Ask for fully qualified domain name validateFqdn := func(input string) error { if input != "" && !govalidator.IsDNSName(input) { return errors.New("invalid domain") } return nil } promptFqdn := promptui.Prompt{ Validate: validateFqdn, Label: "Choose fully-qualified domain name", Default: cfg.FQDN, } if promptFqdnString, err := promptFqdn.Run(); err == nil { if promptFqdnString != "" { v.Set("fqdn", promptFqdnString) } } promptPath := promptui.Prompt{ Label: "Choose URL path, empty means random", Default: cfg.Path, } if promptPathResultString, err := promptPath.Run(); err == nil { if promptPathResultString != "" { v.Set("path", promptPathResultString) } } // Ask for keep alive promptKeepAlive := promptui.Select{ Items: []string{"No", "Yes"}, Label: "Should the server keep alive after transferring?", } if _, promptKeepAliveResultString, err := promptKeepAlive.Run(); err == nil { if promptKeepAliveResultString == "Yes" { v.Set("keepAlive", true) } } // HTTPS // Ask if path is readable and is a file pathIsReadableFile := func(input string) error { if input == "" { return errors.New("invalid path") } path, err := filepath.Abs(util.Expand(input)) if err != nil { return err } fmt.Println(path) fileinfo, err := os.Stat(path) if err != nil { return err } if fileinfo.Mode().IsDir() { return fmt.Errorf("%s is a directory", input) } return nil } promptSecure := promptui.Select{ Items: []string{"No", "Yes"}, Label: "Should files be securely transferred with HTTPS?", } if _, promptSecureResultString, err := promptSecure.Run(); err == nil { if promptSecureResultString == "Yes" { v.Set("secure", true) } cfg.Secure = v.GetBool("secure") } if cfg.Secure { // TLS Cert promptTlsCert := promptui.Prompt{ Label: "Choose TLS certificate path. Empty if not using HTTPS.", Default: cfg.TlsCert, Validate: pathIsReadableFile, } if promptTlsCertString, err := promptTlsCert.Run(); err == nil { v.Set("tls-cert", util.Expand(promptTlsCertString)) } // TLS key promptTlsKey := promptui.Prompt{ Label: "Choose TLS certificate key. Empty if not using HTTPS.", Default: cfg.TlsKey, Validate: pathIsReadableFile, } if promptTlsKeyString, err := promptTlsKey.Run(); err == nil { v.Set("tls-key", util.Expand(promptTlsKeyString)) } } validateIsDir := func(input string) error { if input == "" { return nil } path, err := filepath.Abs(input) if err != nil { return err } f, err := os.Stat(path) if err != nil { return err } if !f.IsDir() { return errors.New("path is not a directory") } return nil } // Ask for default output directory promptOutput := promptui.Prompt{ Label: "Choose default output directory for received files, empty does not set a default", Default: cfg.Output, Validate: validateIsDir, } if promptOutputResultString, err := promptOutput.Run(); err == nil { if promptOutputResultString != "" { output, _ := filepath.Abs(promptOutputResultString) v.Set("output", output) } } promptReversed := promptui.Select{ Items: []string{"No", "Yes"}, Label: "Reverse QR code (black text on white background)?", } if _, promptReversedResultString, err := promptReversed.Run(); err == nil { if promptReversedResultString == "Yes" { v.Set("reversed", true) } cfg.Reversed = v.GetBool("reversed") } return v.WriteConfig() } ================================================ FILE: config/config_test.go ================================================ package config import ( "os" "path/filepath" "reflect" "runtime" "testing" "github.com/claudiodangelis/qrcp/application" ) func TestNew(t *testing.T) { os.Clearenv() _, f, _, _ := runtime.Caller(0) foundIface, err := chooseInterface(application.Flags{}) if err != nil { panic(err) } testdir := filepath.Join(filepath.Dir(f), "testdata") tempfile, err := os.CreateTemp("", "qrcp*tmp.yml") if err != nil { t.Skip() } defer os.Remove(tempfile.Name()) partialconfig, err := os.CreateTemp("", "qrcp*partial.yml") if err != nil { panic(err) } defer os.Remove(partialconfig.Name()) if err := os.WriteFile(partialconfig.Name(), []byte(`port: 9090`), os.ModePerm); err != nil { panic(err) } type args struct { app application.App } tests := []struct { name string args args want Config }{ { "partial", args{ app: application.App{ Flags: application.Flags{ Config: partialconfig.Name(), }, }, }, Config{ Interface: foundIface, Port: 9090, }, }, { "init", args{ app: application.App{ Flags: application.Flags{ Config: tempfile.Name(), }, }, }, Config{ Interface: foundIface, }, }, { "#2", args{ app: application.App{ Flags: application.Flags{ Config: filepath.Join(testdir, "qrcp.yml"), }, }, }, Config{ Interface: foundIface, }, }, { "#2", args{ app: application.App{ Flags: application.Flags{ Config: filepath.Join(testdir, "full.yml"), }, }, }, Config{ Interface: foundIface, Port: 18080, KeepAlive: false, Bind: "10.20.30.40", Path: "random", Secure: false, TlsKey: "/path/to/key", TlsCert: "/path/to/cert", FQDN: "mylan.com", Output: "/path/to/default/output/dir", Reversed: true, }, }, { "overrides", args{ app: application.App{ Flags: application.Flags{ Config: filepath.Join(testdir, "full.yml"), Port: 99999, }, }, }, Config{ Interface: foundIface, Port: 99999, Bind: "10.20.30.40", KeepAlive: false, Path: "random", Secure: false, TlsKey: "/path/to/key", TlsCert: "/path/to/cert", FQDN: "mylan.com", Output: "/path/to/default/output/dir", Reversed: true, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := New(tt.args.app) got.Interface = foundIface if !reflect.DeepEqual(got, tt.want) { t.Errorf("New() = %v, want %v", got, tt.want) } }) } } ================================================ FILE: config/migrate.go ================================================ package config import ( "encoding/json" "os" "path/filepath" "github.com/adrg/xdg" "github.com/claudiodangelis/qrcp/application" "gopkg.in/yaml.v2" ) // Migrate function will look for an existing legacy configuration file // and will migrate it to the new format func Migrate(app application.App) (bool, error) { oldConfigFile := filepath.Join(xdg.ConfigHome, "qrcp", "config.json") newConfigFile := filepath.Join(xdg.ConfigHome, "qrcp", "config.yml") // Check if old configuration file exists if _, err := os.Stat(oldConfigFile); os.IsNotExist(err) { return false, nil } oldConfigFileBytes, err := os.ReadFile(oldConfigFile) if err != nil { panic(err) } var cfg Config if err := json.Unmarshal(oldConfigFileBytes, &cfg); err != nil { panic(err) } newConfigFileBytes, err := yaml.Marshal(cfg) if err != nil { panic(err) } if err := os.WriteFile(newConfigFile, newConfigFileBytes, 0644); err != nil { panic(err) } // Delete old file if err := os.Remove(oldConfigFile); err != nil { panic(err) } return true, nil } ================================================ FILE: config/testdata/full.yml ================================================ interface: __PLACEHOLDER_INTERFACE__ port: 18080 bind: '10.20.30.40' keepAlive: false path: random secure: false tls-key: /path/to/key tls-cert: /path/to/cert fqdn: mylan.com output: /path/to/default/output/dir reversed: true ================================================ FILE: config/testdata/qrcp.yml ================================================ interface: __PLACEHOLDER_INTERFACE__ ================================================ FILE: config/util.go ================================================ package config import ( "errors" "fmt" "github.com/claudiodangelis/qrcp/application" "github.com/claudiodangelis/qrcp/util" "github.com/manifoldco/promptui" ) func chooseInterface(flags application.Flags) (string, error) { interfaces, err := util.Interfaces(flags.ListAllInterfaces) if err != nil { return "", err } if len(interfaces) == 0 { return "", errors.New("no interfaces found") } if len(interfaces) == 1 && !interactive { for name := range interfaces { fmt.Printf("only one interface found: %s, using this one\n", name) return name, nil } } // Map for pretty printing m := make(map[string]string) items := []string{} for name, ip := range interfaces { label := fmt.Sprintf("%s (%s)", name, ip) m[label] = name items = append(items, label) } // Add the "any" interface anyIP := "0.0.0.0" anyName := "any" anyLabel := fmt.Sprintf("%s (%s)", anyName, anyIP) m[anyLabel] = anyName items = append(items, anyLabel) prompt := promptui.Select{ Items: items, Label: "Choose interface", } _, result, err := prompt.Run() if err != nil { return "", err } return m[result], nil } ================================================ FILE: docs/CNAME ================================================ qrcp.sh ================================================ FILE: docs/LICENSE ================================================ MIT License Copyright (c) 2018 Claudio d'Angelis Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: docs/_config.yml ================================================ theme: jekyll-theme-cayman ================================================ FILE: docs/index.md ================================================ ![Logo](img/logo.svg) # $ qrcp Transfer files over Wi-Fi from your computer to a mobile device by scanning a QR code without leaving the terminal. [![Go Report Card](https://goreportcard.com/badge/github.com/claudiodangelis/qrcp)](https://goreportcard.com/report/github.com/claudiodangelis/qrcp) 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. --- ## How does it work? ![Screenshot](img/screenshot.png) `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. The tool prints a QR code that encodes the text: ``` http://{address}:{port}/{random_path} ``` 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. ### Demo **Send files to mobile:** ![screenshot](img/demo.gif) **Receive files from mobile:** ![Screenshot](img/mobile-demo.gif) --- ## Installation ### Using Go (Latest Development Version) Requires Go 1.18 or later: ```sh go install github.com/claudiodangelis/qrcp@latest ``` ### Prebuilt Binaries Download the latest release for your platform from the [Releases](https://github.com/claudiodangelis/qrcp/releases) page. | Platform | Instructions | |-------------|------------------------------------------------------------------------------------------| | **Linux** | Extract the `.tar.gz` archive, move the binary to `/usr/local/bin`, and set permissions. | | **Windows** | Extract the `.tar.gz` archive and place the `.exe` file in a directory in your `PATH`. | | **macOS** | Extract the `.tar.gz` archive, move the binary to `/usr/local/bin`, and set permissions. | ### Package Managers | Platform | Package Manager | Command | |-------------|-----------------|------------------------------------------------| | **Linux** | ArchLinux (AUR) | `yay -S qrcp-bin` or `yay -S qrcp` | | **Linux** | Debian/Ubuntu | `sudo dpkg -i qrcp__linux_x86_64.deb` | | **Linux** | CentOS/Fedora | `sudo rpm -i qrcp__linux_x86_64.rpm` | | **Windows** | WinGet | `winget install --id=claudiodangelis.qrcp -e` | | **Windows** | Scoop | `scoop install qrcp` | | **Windows** | Chocolatey | `choco install qrcp` | | **macOS** | Homebrew | `brew install qrcp` | ### Confirm Installation After installation, verify that `qrcp` is working: ```sh qrcp --help ``` --- ## Usage ### Send Files | Action | Command Example | |-----------------------------|-----------------------------------| | **Send a file** | `qrcp MyDocument.pdf` | | **Send multiple files** | `qrcp MyDocument.pdf IMG0001.jpg` | | **Send a folder** | `qrcp Documents/` | | **Zip before transferring** | `qrcp --zip LongVideo.avi` | ### Receive Files | Action | Command Example | |-------------------------------------|----------------------------------| | **Receive to current directory** | `qrcp receive` | | **Receive to a specific directory** | `qrcp receive --output=/tmp/dir` | --- ## Configuration `qrcp` works without prior configuration, but you can customize it using a configuration file or environment variables. ### Configuration File The default configuration file is stored in `$XDG_CONFIG_HOME/qrcp/config.yml`. You can specify a custom location using the `--config` flag: ```sh qrcp --config /tmp/qrcp.yml MyDocument.pdf ``` ### Configuration Options | Key | Type | Description | |-------------|---------|--------------------------------------------------------------------------------| | `interface` | String | Network interface to bind the web server to. Use `any` to bind to `0.0.0.0`. | | `bind` | String | Address to bind the web server to. Overrides `interface`. | | `port` | Integer | Port to use. Defaults to a random port. | | `path` | String | Path to use in the URL. Defaults to a random string. | | `output` | String | Default directory to receive files. Defaults to the current working directory. | | `fqdn` | String | Fully qualified domain name to use in the URL instead of the IP address. | | `keep-alive` | Bool | Keep the server alive after transferring files. Defaults to `false`. | | `secure` | Bool | Use HTTPS instead of HTTP. Defaults to `false`. | | `tls-cert` | String | Path to the TLS certificate. Used only when `secure: true`. | | `tls-key` | String | Path to the TLS key. Used only when `secure: true`. | ### Environment Variables All configuration parameters can also be set via environment variables prefixed with `QRCP_`: - `$QRCP_INTERFACE` - `$QRCP_PORT` - `$QRCP_KEEPALIVE` --- ## Advanced Usage ### Network Interface To use a specific network interface: ```sh qrcp -i tun0 MyDocument.pdf ``` To bind the web server to all interfaces: ```sh qrcp -i any MyDocument.pdf ``` ### HTTPS Enable secure transfers with HTTPS by providing a TLS certificate and key: ```sh qrcp --tls-cert /path/to/cert.pem --tls-key /path/to/cert.key MyDocument.pdf ``` --- ## Shell Completion `qrcp` provides shell completion scripts for Bash, Zsh, and Fish. | Shell | Command Example | |----------|---------------------------------------------| | **Bash** | `source <(qrcp completion bash)` | | **Zsh** | `qrcp completion zsh > "${fpath[1]}/_qrcp"` | | **Fish** | `qrcp completion fish | source` | --- ## Authors and Credits - **Author**: [Claudio d'Angelis](https://t.me/claudiodangelis) - **Logo**: Provided by [@arasatasaygin](https://github.com/arasatasaygin) as part of the [openlogos](https://github.com/arasatasaygin/openlogos) initiative. - **Releases**: Managed with [goreleaser](https://goreleaser.com). --- ## Clones and Similar Projects | Project Name | Description | |----------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------| | [qr-fileshare](https://github.com/shivensinha4/qr-fileshare) | A similar idea executed in NodeJS with a React interface. | | [instant-file-transfer](https://github.com/maximumdata/instant-file-transfer) _(Uncredited)_ | Node.js project similar to this. | | [qr-filetransfer](https://github.com/sdushantha/qr-filetransfer) | Python clone of this project. | | [qr-filetransfer](https://github.com/svenkatreddy/qr-filetransfer) | Another Node.js clone of this project. | | [qr-transfer-node](https://github.com/codezoned/qr-transfer-node) | Another Node.js clone of this project. | | [QRDELIVER](https://github.com/realdennis/qrdeliver) | Node.js project similar to this. | | [qrfile](https://github.com/sgbj/qrfile) | Transfer files by scanning a QR code. | | [quick-transfer](https://github.com/CodeMan99/quick-transfer) | Node.js clone of this project. | | [share-file-qr](https://github.com/pwalch/share-file-qr) | Python re-implementation of this project. | | [share-files](https://github.com/antoaravinth/share-files) _(Uncredited)_ | Yet another Node.js clone of this project. | | [ezshare](https://github.com/mifi/ezshare) | Another Node.js two-way file sharing tool supporting folders and multiple files. | | [local_file_share](https://github.com/woshimanong1990/local_file_share) | _"Share local file to other people, OR smartphone download files which is in PC."_ | | [qrcp](https://github.com/pearl2201/qrcp) | A desktop app clone of `qrcp`, written with C# and .NET Core, works for Windows. | | [swift_file](https://github.com/mateoradman/swift_file) | Rust project inspired by `qrcp`. | | [qrcp-android](https://github.com/ianfixes/qrcp-android) | Android app inspired by `qrcp`. | --- ## License MIT. See [LICENSE](LICENSE). ================================================ FILE: docs/tutorials/secure-transfers-with-mkcert.md ================================================ # Secure transfers with mkcert _Published on October 14th, 2020, by [Claudio d'Angelis](https://claudiodangelis.com)._ 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._ 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: | Name | Value | | --- | --- | | IP of the computer/laptop | `192.168.1.107` | | Root certificate path | `~/.local/share/mkcert/rootCA.pem` | | Certificate path | `~/certs/192.168.1.107.pem` | | Certificate key | `~/certs/192.168.1.107-key.pem` | | Transferred file | `MyDocument.pdf` | **Note**: secure transfers are only supported by `qrcp` version `0.7.0` and above. ## Generate certificates Open your terminal, create a directory called `certs` and change to it Install **mkcert** _(refer to the [README](https://github.com/FiloSottile/mkcert#installation))_. Generate the Certificate Authority: ``` mkcert --install ``` If everything worked correctly, you should see the similar output: ``` Created a new local CA at "/home/me/.local/share/mkcert" Sudo password: The local CA is now installed in the system trust store! ⚡️ The local CA is now installed in the Firefox and/or Chrome/Chromium trust store (requires browser restart)! ``` Generate a certificate for the IP of your computer/laptop: ``` mkcert 192.168.1.107 ``` You should see a similar output: ``` Using the local CA at "/home/me/.local/share/mkcert" Created a new certificate valid for the following names - "192.168.1.107" The certificate is at "./192.168.1.107.pem" and the key at "./192.168.1.107-key.pem" ``` 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: ``` mkcert --CAROOT ``` 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). 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. ``` openssl x509 -inform PEM -outform DER -in $(mkcert --CAROOT)/rootCA.pem -out $(mkcert --CAROOT)/rootCA.der.crt ``` 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". ## Transfer something! You are now ready to securely transfer a file using the certificates generated by mkcert: ``` qrcp --tls-cert ~/certs/192.168.1.107.pem --tls-key ~/certs/192.168.1.107-key.pem screenshot.png ``` The output will be something like this (note that the printed URL starts with "https"): ``` Scan the following URL with a QR reader to start the file transfer: https://192.168.1.107:40221/send/ljd7 █████████████████████████████████████ █████████████████████████████████████ ████ ▄▄▄▄▄ █ █▀▀ █▀ ▄█▄▀▀█ ▄▄▄▄▄ ████ ████ █ █ █▀█▄▄▄▄▄▄█▄█▄██ █ █ ████ ████ █▄▄▄█ ██▀▄▄▀▄▀█▀█ ▀▄█ █▄▄▄█ ████ ████▄▄▄▄▄▄▄█ ▀▄█ ▀ ▀ ▀ █ █▄▄▄▄▄▄▄████ ████ ▀▄ ▀▄▄ ▀██▀▄▄▄▄ ▄█▀▄██▄█▄ ████ ████▀▀ █▄▀▀ ▄▄█▀ █▀▄█▄▀ ▄▄██▄▄▀████ ████▀█ ▀█▄▄█ ▄██ ▀██▀▀ ▄█▀▀█▄ ▀▀████ ████ █▀█▀▄ █ ▄ ▀▄ ▀▄██ █▀█▄ ▄████ █████ ▀ ▀▀▄ ▄▀█ ▀██▄█▄▀▄██ ▀ ▀ █████ ████▄████▄▄ ▀█ ▀████▄█▄ ▄▀█ █ ██████ █████▄█▄▄▄▄█▀ ▀█ ▀ ▀▀ ▄▄▄ ▀▀████ ████ ▄▄▄▄▄ █ █▄▄▄█ █▀█ █▄█ █▄▀████ ████ █ █ █▀ ▀ ▄▀ █ █▀ ▄ ▄▀▄▄▄████ ████ █▄▄▄█ █▄█▄█ ▀▀▀▄▀ █▄▄ █▀█▄▀▄████ ████▄▄▄▄▄▄▄█▄▄▄██▄▄▄█▄▄██▄█▄█▄▄▄█████ █████████████████████████████████████ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ``` Scan the QR and the file will be securely transferred to your mobile. Congratulations! ## Configure qrcp to default to those value 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: _(**Note**: you must enter the **absolute path** of the certificates)_ - Should files be securely transferred with HTTPS? **Yes** - Choose TLS certificate path. Empty if not using HTTPS **/home/user/certs/192.168.1.107.pem** - Choose TLS certificate key. Empty if not using HTTPS **/home/user/certs/192.168.1.107-key.pem** After configuring qrcp, all transfers will be always secured. If you'll want to temporarily disable security, just add the `--secure=false` flag. Secure transfer: ``` qrcp MyDocument.pdf ``` Insecure transfer: ``` qrcp --secure=false MyDocument.pdf ``` ## Conclusion In this tutorial we have seen how to: - create a local certificate authority with `mkcert` - issue a certificate for your computer/laptop - use that certificate to secure the transferring of a file with `qrcp` 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. ## Useful Links - [qrcp homepage](https://github.com/claudiodangelis/qrcp) - [mkcert homepage](https://github.com/FiloSottile/mkcert) - [qrcp Telegram channel](https://t.me/qrcp_dev) ================================================ FILE: docs/update-docs.sh ================================================ #!/bin/bash if ! [[ $(git rev-parse --show-toplevel 2>/dev/null) = "$PWD" ]]; then echo "error: script should be run from the root of the repository" exit 1 fi cp README.md docs/index.md find docs -name "*.md" | while read page do sed -i 's/docs\/img/img/g' $page done ================================================ FILE: go.mod ================================================ module github.com/claudiodangelis/qrcp go 1.21.0 toolchain go1.24.1 require ( github.com/adrg/xdg v0.5.3 github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 github.com/eiannone/keyboard v0.0.0-20200508000154-caf4b762e807 github.com/glendc/go-external-ip v0.1.0 github.com/jhoonb/archivex v0.0.0-20180718040744-0488e4ce1681 github.com/manifoldco/promptui v0.9.0 github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086 github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.20.0 gopkg.in/cheggaaa/pb.v1 v1.0.28 gopkg.in/yaml.v2 v2.4.0 ) require ( github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect github.com/fatih/color v1.18.0 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.12.0 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: go.sum ================================================ github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06GQ59hwDQAvmK1qxOQGB3WuVTRoY0okPTAv0= github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/eiannone/keyboard v0.0.0-20200508000154-caf4b762e807 h1:jdjd5e68T4R/j4PWxfZqcKY8KtT9oo8IPNVuV4bSXDQ= github.com/eiannone/keyboard v0.0.0-20200508000154-caf4b762e807/go.mod h1:Xoiu5VdKMvbRgHuY7+z64lhu/7lvax/22nzASF6GrO8= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/glendc/go-external-ip v0.1.0 h1:iX3xQ2Q26atAmLTbd++nUce2P5ht5P4uD4V7caSY/xg= github.com/glendc/go-external-ip v0.1.0/go.mod h1:CNx312s2FLAJoWNdJWZ2Fpf5O4oLsMFwuYviHjS4uJE= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jhoonb/archivex v0.0.0-20180718040744-0488e4ce1681 h1:EiEjLram6Y0WXygV4WyzKmTr3XaR4CD3tvjdTrsk3cU= github.com/jhoonb/archivex v0.0.0-20180718040744-0488e4ce1681/go.mod h1:GN1Mg/uXQ6qwXA0HypnUO3xlcQJS9/y68EsHNeuuRa4= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086 h1:RYiqpb2ii2Z6J4x0wxK46kvPBbFuZcdhS+CIztmYgZs= github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY= github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/cheggaaa/pb.v1 v1.0.28 h1:n1tBJnnK2r7g9OW2btFH91V92STTUevLXYFb8gy9EMk= gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: logger/logger.go ================================================ package logger import ( "fmt" ) // Print prints its argument if the --quiet flag is not passed func (l Logger) Print(args ...interface{}) { if !l.quiet { fmt.Println(args...) } } // Logger struct type Logger struct { quiet bool } // New logger func New(quiet bool) Logger { return Logger{ quiet: quiet, } } ================================================ FILE: main.go ================================================ package main import ( "os" "github.com/claudiodangelis/qrcp/cmd" ) func main() { if err := cmd.Execute(); err != nil { os.Exit(1) } } ================================================ FILE: pages/pages.go ================================================ package pages // Upload page var Upload = ` qrcp

Send files or text

` // Done page var Done = ` qrcp
` ================================================ FILE: qr/qr.go ================================================ package qr import ( "fmt" "image" "log" "github.com/skip2/go-qrcode" ) // RenderString as a QR code func RenderString(s string, inverseColor bool) { q, err := qrcode.New(s, qrcode.Medium) if err != nil { log.Fatal(err) } fmt.Println(q.ToSmallString(inverseColor)) } // RenderImage returns a QR code as an image.Image func RenderImage(s string) image.Image { q, err := qrcode.New(s, qrcode.Medium) if err != nil { log.Fatal(err) } return q.Image(256) } ================================================ FILE: server/server.go ================================================ package server import ( "context" "crypto/tls" "fmt" "image/jpeg" "io" "log" "net" "net/http" "net/url" "os" "os/exec" "os/signal" "path/filepath" "runtime" "strings" "sync" "github.com/claudiodangelis/qrcp/qr" "github.com/claudiodangelis/qrcp/body" "github.com/claudiodangelis/qrcp/config" "github.com/claudiodangelis/qrcp/pages" "github.com/claudiodangelis/qrcp/util" "gopkg.in/cheggaaa/pb.v1" ) // Server is the server type Server struct { BaseURL string // SendURL is the URL used to send the file SendURL string // ReceiveURL is the URL used to Receive the file ReceiveURL string instance *http.Server body body.Body outputDir string stopChannel chan bool // expectParallelRequests is set to true when qrcp sends files, in order // to support downloading of parallel chunks expectParallelRequests bool } // ReceiveTo sets the output directory func (s *Server) ReceiveTo(dir string) error { output, err := filepath.Abs(dir) if err != nil { return err } // Check if the output dir exists fileinfo, err := os.Stat(output) if err != nil { return err } if !fileinfo.IsDir() { return fmt.Errorf("%s is not a valid directory", output) } s.outputDir = output return nil } // Send adds a handler for sending the file func (s *Server) Send(p body.Body) { s.body = p s.expectParallelRequests = true } // DisplayQR creates a handler for serving the QR code in the browser func (s *Server) DisplayQR(url string) { const PATH = "/qr" qrImg := qr.RenderImage(url) http.HandleFunc(PATH, func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "image/jpeg") if err := jpeg.Encode(w, qrImg, nil); err != nil { panic(err) } }) openBrowser(s.BaseURL + PATH) } // Wait for transfer to be completed, it waits forever if kept awlive func (s Server) Wait() error { <-s.stopChannel if err := s.instance.Shutdown(context.Background()); err != nil { log.Println(err) } if s.body.DeleteAfterTransfer { if err := s.body.Delete(); err != nil { panic(err) } } return nil } // Shutdown the server func (s Server) Shutdown() { s.stopChannel <- true } // New instance of the server func New(cfg *config.Config) (*Server, error) { app := &Server{} // Get the address of the configured interface to bind the server to. // If `bind` configuration parameter has been configured, it takes precedence bind, err := util.GetInterfaceAddress(cfg.Interface) if err != nil { return &Server{}, err } if cfg.Bind != "" { bind = cfg.Bind } // Create a listener. If `port: 0`, a random one is chosen listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", bind, cfg.Port)) if err != nil { return nil, err } // Set the value of computed port port := listener.Addr().(*net.TCPAddr).Port // Set the host host := fmt.Sprintf("%s:%d", bind, port) // Get a random path to use path := cfg.Path if path == "" { path = util.GetRandomURLPath() } // Set the hostname hostname := fmt.Sprintf("%s:%d", bind, port) // Use external IP when using `interface: any`, unless a FQDN is set if bind == "0.0.0.0" && cfg.FQDN == "" { fmt.Println("Retrieving the external IP...") extIP, err := util.GetExternalIP() if err != nil { panic(err) } extIPString := extIP.String() fmtstring := "%s:%d" if strings.Count(extIPString, ":") >= 2 { // IPv6 address, wrap it in [] to add a port fmtstring = "[%s]:%d" } hostname = fmt.Sprintf(fmtstring, extIPString, port) } // Use a fully-qualified domain name if set if cfg.FQDN != "" { hostname = fmt.Sprintf("%s:%d", cfg.FQDN, port) } // Set URLs protocol := "http" if cfg.Secure { protocol = "https" } app.BaseURL = fmt.Sprintf("%s://%s", protocol, hostname) app.SendURL = fmt.Sprintf("%s/send/%s", app.BaseURL, path) app.ReceiveURL = fmt.Sprintf("%s/receive/%s", app.BaseURL, path) // Create a server httpserver := &http.Server{ Addr: host, TLSConfig: &tls.Config{ MinVersion: tls.VersionTLS12, CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256}, PreferServerCipherSuites: true, CipherSuites: []uint16{ tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, tls.TLS_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_RSA_WITH_AES_256_CBC_SHA, }, }, TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)), } // Create channel to send message to stop server app.stopChannel = make(chan bool) // Create cookie used to verify request is coming from first client to connect cookie := http.Cookie{Name: "qrcp", Value: ""} // Gracefully shutdown when an OS signal is received or when "q" is pressed sig := make(chan os.Signal, 1) signal.Notify(sig, os.Interrupt) go func() { <-sig app.stopChannel <- true }() // The handler adds and removes from the sync.WaitGroup // When the group is zero all requests are completed // and the server is shutdown var waitgroup sync.WaitGroup waitgroup.Add(1) var initCookie sync.Once // Create handlers // Send handler (sends file to caller) http.HandleFunc("/send/"+path, func(w http.ResponseWriter, r *http.Request) { if !cfg.KeepAlive && strings.HasPrefix(r.Header.Get("User-Agent"), "Mozilla") { if cookie.Value == "" { initCookie.Do(func() { value, err := util.GetSessionID() if err != nil { log.Println("Unable to generate session ID", err) app.stopChannel <- true return } cookie.Value = value http.SetCookie(w, &cookie) }) } else { // Check for the expected cookie and value // If it is missing or doesn't match // return a 400 status rcookie, err := r.Cookie(cookie.Name) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if rcookie.Value != cookie.Value { http.Error(w, "mismatching cookie", http.StatusBadRequest) return } // If the cookie exits and matches // this is an aadditional request. // Increment the waitgroup waitgroup.Add(1) } // Remove connection from the waitgroup when done defer waitgroup.Done() } w.Header().Set("Content-Disposition", "attachment; filename=\""+ app.body.Filename+ "\"; filename*=UTF-8''"+ url.QueryEscape(app.body.Filename)) http.ServeFile(w, r, app.body.Path) }) // Upload handler (serves the upload page) http.HandleFunc("/receive/"+path, func(w http.ResponseWriter, r *http.Request) { htmlVariables := struct { Route string File string }{} htmlVariables.Route = "/receive/" + path switch r.Method { case "POST": filenames := util.ReadFilenames(app.outputDir) reader, err := r.MultipartReader() if err != nil { fmt.Fprintf(w, "Upload error: %v\n", err) log.Printf("Upload error: %v\n", err) app.stopChannel <- true return } transferredFiles := []string{} progressBar := pb.New64(r.ContentLength) progressBar.ShowCounters = false for { part, err := reader.NextPart() if err == io.EOF { break } // iIf part.FileName() is empty, skip this iteration. if part.FileName() == "" { continue } // Prepare the destination fileName := getFileName(filepath.Base(part.FileName()), filenames) out, err := os.Create(filepath.Join(app.outputDir, fileName)) if err != nil { // Output to server fmt.Fprintf(w, "Unable to create the file for writing: %s\n", err) // Output to console log.Printf("Unable to create the file for writing: %s\n", err) // Send signal to server to shutdown app.stopChannel <- true return } defer out.Close() // Add name of new file filenames = append(filenames, fileName) // Write the content from POSTed file to the out fmt.Println("Transferring file: ", out.Name()) progressBar.Prefix(out.Name()) progressBar.Start() buf := make([]byte, 1024) for { // Read a chunk n, err := part.Read(buf) if err != nil && err != io.EOF { // Output to server fmt.Fprintf(w, "Unable to write file to disk: %v", err) // Output to console fmt.Printf("Unable to write file to disk: %v", err) // Send signal to server to shutdown app.stopChannel <- true return } if n == 0 { break } // Write a chunk if _, err := out.Write(buf[:n]); err != nil { // Output to server fmt.Fprintf(w, "Unable to write file to disk: %v", err) // Output to console log.Printf("Unable to write file to disk: %v", err) // Send signal to server to shutdown app.stopChannel <- true return } progressBar.Add(n) } transferredFiles = append(transferredFiles, out.Name()) } progressBar.FinishPrint("File transfer completed") // Set the value of the variable to the actually transferred files htmlVariables.File = strings.Join(transferredFiles, ", ") serveTemplate("done", pages.Done, w, htmlVariables) if !cfg.KeepAlive { app.stopChannel <- true } case "GET": serveTemplate("upload", pages.Upload, w, htmlVariables) } }) // Wait for all wg to be done, then send shutdown signal go func() { waitgroup.Wait() if cfg.KeepAlive || !app.expectParallelRequests { return } app.stopChannel <- true }() go func() { netListener := tcpKeepAliveListener{listener.(*net.TCPListener)} if cfg.Secure { if err := httpserver.ServeTLS(netListener, cfg.TlsCert, cfg.TlsKey); err != http.ErrServerClosed { log.Fatalln("error starting the server:", err) } } else { if err := httpserver.Serve(netListener); err != http.ErrServerClosed { log.Fatalln("error starting the server", err) } } }() app.instance = httpserver return app, nil } // openBrowser navigates to a url using the default system browser func openBrowser(url string) { var err error switch runtime.GOOS { case "linux": err = exec.Command("xdg-open", url).Start() case "windows": err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() case "darwin": err = exec.Command("open", url).Start() default: err = fmt.Errorf("failed to open browser on platform: %s", runtime.GOOS) } if err != nil { log.Fatal(err) } } ================================================ FILE: server/tcpkeepalivelistener.go ================================================ package server // Copyright (c) 2009 The Go Authors. All rights reserved. // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above // copyright notice, this list of conditions and the following disclaimer // in the documentation and/or other materials provided with the // distribution. // * Neither the name of Google Inc. nor the names of its // contributors may be used to endorse or promote products derived from // this software without specific prior written permission. // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import ( "net" "time" ) // tcpKeepAliveListener applies TCP keepalives to the listener type tcpKeepAliveListener struct { *net.TCPListener } // Accept accepts TCP func (ln tcpKeepAliveListener) Accept() (net.Conn, error) { tc, err := ln.AcceptTCP() if err != nil { return nil, err } if err := tc.SetKeepAlive(true); err != nil { panic(err) } if err := tc.SetKeepAlivePeriod(3 * time.Minute); err != nil { panic(err) } return tc, nil } ================================================ FILE: server/util.go ================================================ package server import ( "fmt" "html/template" "io" "path/filepath" "strings" ) func serveTemplate(name string, tmpl string, w io.Writer, data interface{}) { t, err := template.New(name).Parse(tmpl) if err != nil { panic(err) } if err := t.Execute(w, data); err != nil { panic(err) } } // getFileName generates a file name based on the existing files in the directory // if name isn't taken leave it unchanged // else change name to format "name(number).ext" func getFileName(newFilename string, fileNamesInTargetDir []string) string { fileExt := filepath.Ext(newFilename) fileName := strings.TrimSuffix(newFilename, fileExt) number := 1 i := 0 for i < len(fileNamesInTargetDir) { if newFilename == fileNamesInTargetDir[i] { newFilename = fmt.Sprintf("%s(%v)%s", fileName, number, fileExt) number++ i = 0 } i++ } return newFilename } ================================================ FILE: util/net.go ================================================ package util import ( "net" "regexp" externalip "github.com/glendc/go-external-ip" ) // Interfaces returns a `name:ip` map of the suitable interfaces found func Interfaces(listAll bool) (map[string]string, error) { names := make(map[string]string) ifaces, err := net.Interfaces() if err != nil { return names, err } var re = regexp.MustCompile(`^(veth|br\-|docker|lo|EHC|XHC|bridge|gif|stf|p2p|awdl|utun|tun|tap)`) for _, iface := range ifaces { if !listAll && re.MatchString(iface.Name) { continue } if iface.Flags&net.FlagUp == 0 { continue } ip, err := FindIP(iface) if err != nil { continue } names[iface.Name] = ip } return names, nil } // GetExternalIP of this host func GetExternalIP() (net.IP, error) { consensus := externalip.DefaultConsensus(nil, nil) // Get your IP, which is never when err is ip, err := consensus.ExternalIP() if err != nil { return nil, err } return ip, nil } ================================================ FILE: util/util.go ================================================ package util import ( "crypto/rand" "encoding/base64" "errors" "io" "net" "os" "os/user" "path/filepath" "runtime" "strconv" "strings" "time" "github.com/jhoonb/archivex" ) // Expand tilde in paths func Expand(input string) string { if runtime.GOOS == "windows" { return input } usr, _ := user.Current() dir := usr.HomeDir if input == "~" { input = dir } else if strings.HasPrefix(input, "~/") { input = filepath.Join(dir, input[2:]) } return input } // ZipFiles and return the resulting zip's filename func ZipFiles(files []string) (string, error) { zip := new(archivex.ZipFile) tmpfile, err := os.CreateTemp("", "qrcp") if err != nil { return "", err } tmpfile.Close() if err := os.Rename(tmpfile.Name(), tmpfile.Name()+".zip"); err != nil { return "", err } tmpfile, err = os.OpenFile(tmpfile.Name()+".zip", os.O_RDWR, 0644) if err != nil { return "", err } if err := zip.CreateWriter(tmpfile.Name(), tmpfile); err != nil { return "", err } for _, filename := range files { fileinfo, err := os.Stat(filename) if err != nil { return "", err } if fileinfo.IsDir() { if err := zip.AddAll(filename, true); err != nil { return "", err } } else { file, err := os.Open(filename) if err != nil { return "", err } defer file.Close() if err := zip.Add(filename, file, fileinfo); err != nil { return "", err } } } if err := zip.Writer.Close(); err != nil { return "", err } if err := tmpfile.Close(); err != nil { return "", err } return zip.Name, nil } // GetRandomURLPath returns a random string of 4 alphanumeric characters func GetRandomURLPath() string { timeNum := time.Now().UTC().UnixNano() alphaString := strconv.FormatInt(timeNum, 36) return alphaString[len(alphaString)-4:] } // GetSessionID returns a base64 encoded string of 40 random characters func GetSessionID() (string, error) { randbytes := make([]byte, 40) if _, err := io.ReadFull(rand.Reader, randbytes); err != nil { return "", err } return base64.StdEncoding.EncodeToString(randbytes), nil } // GetInterfaceAddress returns the address of the network interface to // bind the server to. If the interface is "any", it will return 0.0.0.0. // If no interface is found with that name, an error is returned func GetInterfaceAddress(ifaceString string) (string, error) { if ifaceString == "any" { return "0.0.0.0", nil } ifaces, err := net.Interfaces() if err != nil { return "", err } var candidateInterface *net.Interface for _, iface := range ifaces { if iface.Name == ifaceString { candidateInterface = &iface break } } if candidateInterface != nil { ip, err := FindIP(*candidateInterface) if err != nil { return "", err } return ip, nil } return "", errors.New("unable to find interface") } // FindIP returns the IP address of the passed interface, and an error func FindIP(iface net.Interface) (string, error) { var ip string addrs, err := iface.Addrs() if err != nil { return "", err } for _, addr := range addrs { if ipnet, ok := addr.(*net.IPNet); ok { if ipnet.IP.IsLinkLocalUnicast() { continue } if ipnet.IP.To4() != nil { ip = ipnet.IP.String() continue } // Use IPv6 only if an IPv4 hasn't been found yet. // This is eventually overwritten with an IPv4, if found (see above) if ip == "" { ip = "[" + ipnet.IP.String() + "]" } } } if ip == "" { return "", errors.New("unable to find an IP for this interface") } return ip, nil } // ReadFilenames from dir func ReadFilenames(dir string) []string { files, err := os.ReadDir(dir) if err != nil { panic(err) } // Create array of names of files which are stored in dir // used later to set valid name for received files filenames := make([]string, 0, len(files)) for _, fi := range files { filenames = append(filenames, fi.Name()) } return filenames } ================================================ FILE: version/version.go ================================================ package version import "fmt" var ( app = "qrcp" version = "dev" date = "n/a" ) // String returns a string representation of the build. func String() string { return fmt.Sprintf("%s %s [date: %s]", app, version, date) }