Full Code of troglobit/uftpd for AI

master 5de46f101367 cached
46 files
137.2 KB
42.6k tokens
101 symbols
1 requests
Download .txt
Repository: troglobit/uftpd
Branch: master
Commit: 5de46f101367
Files: 46
Total size: 137.2 KB

Directory structure:
gitextract_aq77ie1o/

├── .github/
│   ├── CODE-OF-CONDUCT.md
│   ├── CONTRIBUTING.md
│   ├── SECURITY.md
│   └── workflows/
│       ├── build.yml
│       ├── coverity.yml
│       └── release.yml
├── .gitignore
├── ChangeLog.md
├── LICENSE
├── Makefile.am
├── README.md
├── autogen.sh
├── configure.ac
├── debian/
│   ├── .gitignore
│   ├── README.Debian
│   ├── changelog
│   ├── compat
│   ├── config
│   ├── control
│   ├── copyright
│   ├── dirs
│   ├── docs
│   ├── postinst
│   ├── postrm
│   ├── prerm
│   ├── rules
│   ├── source/
│   │   └── format
│   └── templates
├── doc/
│   └── TODO.md
├── man/
│   ├── Makefile.am
│   └── uftpd.8
├── src/
│   ├── .gitignore
│   ├── Makefile.am
│   ├── common.c
│   ├── ftpcmd.c
│   ├── log.c
│   ├── tftpcmd.c
│   ├── uftpd.c
│   └── uftpd.h
└── test/
    ├── .gitignore
    ├── Makefile.am
    ├── ftp.sh
    ├── lib.sh
    ├── maxfiles.sh
    ├── mlst.sh
    └── tftp.sh

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/CODE-OF-CONDUCT.md
================================================
Contributor Code of Conduct
===========================

As contributors and maintainers of this project, and in the interest of
fostering an open and welcoming community, we pledge to respect all
people who contribute through reporting issues, posting feature
requests, updating documentation, submitting pull requests or patches,
and other activities.

We are committed to making participation in this project a
harassment-free experience for everyone, regardless of level of
experience, gender, gender identity and expression, sexual orientation,
disability, personal appearance, body size, race, ethnicity, age,
religion, or nationality.

Examples of unacceptable behavior by participants include:

* The use of sexualized language or imagery
* Personal attacks
* Trolling or insulting/derogatory comments
* Public or private harassment
* Publishing other's private information, such as physical or electronic
  addresses, without explicit permission
* Other unethical or unprofessional conduct.

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. By adopting
this Code of Conduct, project maintainers commit themselves to fairly
and consistently applying these principles to every aspect of managing
this project. Project maintainers who do not follow or enforce the Code
of Conduct may be permanently removed from the project team.

This code of conduct applies both within project spaces and in public
spaces when an individual is representing the project or its community.

Instances of abusive, harassing, or otherwise unacceptable behavior may
be reported by opening an issue or contacting one or more of the project
maintainers.

This Code of Conduct is adapted from the [Contributor Covenant][1],
[version 1.2.0][2].

[1]: http://contributor-covenant.org
[2]: http://contributor-covenant.org/version/1/2/0/


================================================
FILE: .github/CONTRIBUTING.md
================================================
Contributing to uftpd
=====================

We welcome any and all help in the form of bug reports, fixes, patches
for new features -- *preferably as GitHub pull requests*.  Other methods
are of course also possible: emailing the maintainer a patch or even a
raw file, or simply emailing a feature request or an alert of a problem.
For email questions/requests/alerts there is always the risk of memory
exhaustion on the part of the maintainer(s), so use GitHub :)

If you are unsure of what to do, or how to implement an idea or bugfix,
open an issue with `"[RFC: Unsure if this is a bug ... ?"`, or similar,
so we can discuss it.  Talking about the code first is the best way to
get started before submitting a pull request.

Either way, when sending an email, patch, or pull request, start by
stating the version the change is made against, what it does, and why.

Please take care to ensure you follow the project coding style and the
commit message format.  If you follow these recommendations you help
the maintainer(s) and make it easier for them to include your code.


Coding Style
------------

> **Tip:** Always submit code that follows the style of surrounding code!

First of all, lines are allowed to be longer than 72 characters these
days.  In fact, there exist no enforced maximum, but keeping it around
100 chars is OK.

The coding style itself is strictly Linux [KNF][].


Commit Messages
---------------

Commit messages exist to track *why* a change was made.  Try to be as
clear and concise as possible in your commit messages, and always, be
proud of your work and set up a proper GIT identity for your commits:

    git config --global user.name "Jane Doe"
    git config --global user.email jane.doe@example.com

Example commit message from the [Pro Git][gitbook] online book, notice
how `git commit -s` is used to automatically add a `Signed-off-by`:

    Brief, but clear and concise summary of changes
    
    More detailed explanatory text, if necessary.  Wrap it to about 72
    characters or so.  In some contexts, the first line is treated as
    the subject of an email and the rest of the text as the body.  The
    blank line separating the ummary from the body is critical (unless
    you omit the body entirely); tools like rebase can get confused if
    you run the two together.
    
    Further paragraphs come after blank lines.
    
     - Bullet points are okay, too
    
     - Typically a hyphen or asterisk is used for the bullet, preceded
       by a single space, with blank lines in between, but conventions
       vary here
    
    Signed-off-by: Jane Doe <jane.doe@example.com>


[github]:   https://github.com/troglobit/uftpd/
[KNF]:      https://en.wikipedia.org/wiki/Kernel_Normal_Form
[gitbook]:  https://git-scm.com/book/ch5-2.html


================================================
FILE: .github/SECURITY.md
================================================
# Security Policy

## Supported Versions

uftpd is a small project, as such we have no possibility to support older versions.
The only supported version is the latest released on GitHub:

<https://github.com/troglobit/uftpd/releases>

## Reporting a Vulnerability

Contact the project's main author and owner to report and discuss vulnerabilities.


================================================
FILE: .github/workflows/build.yml
================================================
name: Bob the Builder

# Run on all branches, including all pull requests, except the 'dev'
# branch since that's where we run Coverity Scan (limited tokens/day)
on:
  push:
    branches:
      - '**'
      - '!dev'
  pull_request:
    branches:
      - '**'

jobs:
  build:
    # Verify we can build on latest Ubuntu with both gcc and clang
    name: ${{ matrix.compiler }}
    runs-on: ubuntu-latest
    strategy:
      matrix:
        compiler: [gcc, clang]
      fail-fast: false
    env:
      MAKEFLAGS: -j3
      CC: ${{ matrix.compiler }}
    steps:
      - name: Install dependencies
        run: |
          curl -sS https://deb.troglobit.com/pubkey.gpg | sudo apt-key add -
          echo "deb [arch=amd64] https://deb.troglobit.com/debian stable main" \
               | sudo tee /etc/apt/sources.list.d/troglobit.list
          sudo apt-get -y update
          sudo apt-get -y install tree ftp tnftp tftp-hpa libuev-dev libite-dev
      - uses: actions/checkout@v2
      - name: Configure
        run: |
          ./autogen.sh
          ./configure --prefix=
      - name: Build
        run: |
          make V=1
      - name: Install
        run: |
          DESTDIR=~/tmp make install-strip
          tree ~/tmp
          ldd ~/tmp/sbin/uftpd
          size ~/tmp/sbin/uftpd
          ~/tmp/sbin/uftpd -h
      - name: Test
        run: |
          ulimit -n 1024
          # Tests must currently not run in parallel
          LD_LIBRARY_PATH=/tmp/lib make -j1 check


================================================
FILE: .github/workflows/coverity.yml
================================================
name: Coverity Scan

on:
  push:
    branches:
      - 'dev'

env:
  PROJECT_NAME: uftpd
  CONTACT_EMAIL: troglobit@gmail.com
  COVERITY_NAME: troglobit-uftpd
  COVERITY_PROJ: troglobit%2Fuftpd

jobs:
  coverity:
    runs-on: ubuntu-latest
    env:
      MAKEFLAGS: -j3
    steps:
      - name: Fetch latest Coverity Scan MD5
        id: var
        env:
          TOKEN: ${{ secrets.COVERITY_SCAN_TOKEN }}
        run: |
          wget -q https://scan.coverity.com/download/cxx/linux64         \
               --post-data "token=$TOKEN&project=${COVERITY_PROJ}&md5=1" \
               -O coverity-latest.tar.gz.md5
          export MD5=$(cat coverity-latest.tar.gz.md5)
          echo "Got MD5 $MD5"
          echo ::set-output name=md5::${MD5}
      - uses: actions/cache@v2
        id: cache
        with:
          path: coverity-latest.tar.gz
          key: ${{ runner.os }}-coverity-${{ steps.var.outputs.md5 }}
          restore-keys: |
            ${{ runner.os }}-coverity-${{ steps.var.outputs.md5 }}
            ${{ runner.os }}-coverity-
            ${{ runner.os }}-coverity
      - name: Download Coverity Scan
        env:
          TOKEN: ${{ secrets.COVERITY_SCAN_TOKEN }}
        run: |
          if [ ! -f coverity-latest.tar.gz ]; then
            wget -q https://scan.coverity.com/download/cxx/linux64   \
                 --post-data "token=$TOKEN&project=${COVERITY_PROJ}" \
                 -O coverity-latest.tar.gz
          else
            echo "Latest Coverity Scan available from cache :-)"
            md5sum coverity-latest.tar.gz
          fi
          mkdir coverity
          tar xzf coverity-latest.tar.gz --strip 1 -C coverity
      - name: Install dependencies
        run: |
          curl -sS https://deb.troglobit.com/pubkey.gpg | sudo apt-key add -
          echo "deb [arch=amd64] https://deb.troglobit.com/debian stable main" \
               | sudo tee /etc/apt/sources.list.d/troglobit.list
          sudo apt-get -y update
          sudo apt-get -y install pkg-config libuev-dev libite-dev
      - uses: actions/checkout@v2
      - name: Configure
        run: |
          ./autogen.sh
          ./configure
      - name: Build
        run: |
          export PATH=`pwd`/coverity/bin:$PATH
          cov-build --dir cov-int make
      - name: Submit results to Coverity Scan
        env:
          TOKEN: ${{ secrets.COVERITY_SCAN_TOKEN }}
        run: |
          tar czvf ${PROJECT_NAME}.tgz cov-int
          curl \
            --form project=${COVERITY_NAME} \
            --form token=$TOKEN \
            --form email=${CONTACT_EMAIL} \
            --form file=@${PROJECT_NAME}.tgz \
            --form version=trunk \
            --form description="${PROJECT_NAME} $(git rev-parse HEAD)" \
            https://scan.coverity.com/builds?project=${COVERITY_PROJ}
      - name: Upload build.log
        uses: actions/upload-artifact@v2
        with:
          name: coverity-build.log
          path: cov-int/build-log.txt


================================================
FILE: .github/workflows/release.yml
================================================
name: Release General

on:
  push:
    tags:
      - 'v[0-9]+.[0-9]+*'

jobs:
  release:
    name: Create GitHub release
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/')
    outputs:
      upload_url: ${{ steps.create_release.outputs.upload_url }}
      release_id: ${{ steps.create_release.outputs.id }}
    steps:
      - uses: actions/checkout@v2
      - name: Extract ChangeLog entry ...
        # Hack to extract latest entry for body_path below
        run: |
          awk '/-----*/{if (x == 1) exit; x=1;next}x' ChangeLog.md \
              |head -n -1 > release.md
          cat release.md
      - name: Create release ...
        id: create_release
        uses: actions/create-release@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag_name: ${{ github.ref }}
          release_name: uftpd ${{ github.ref }}
          body_path: release.md
          draft: false
          prerelease: false
  tarball:
    name: Build and upload release tarball
    needs: release
    if: startsWith(github.ref, 'refs/tags/')
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Installing dependencies ...
        run: |
          curl -sS https://deb.troglobit.com/pubkey.gpg | sudo apt-key add -
          echo "deb [arch=amd64] https://deb.troglobit.com/debian stable main" \
               | sudo tee /etc/apt/sources.list.d/troglobit.list
          sudo apt-get -y update
          sudo apt-get -y install ftp tnftp tftp-hpa libuev-dev libite-dev
      - name: Creating Makefiles ...
        run: |
          ./autogen.sh
          ./configure
      - name: Build release ...
        run: |
          make release
          ls -lF ../
          mkdir -p artifacts/
          mv ../*.tar.* artifacts/
      - name: Upload release artifacts ...
        uses: skx/github-action-publish-binaries@release-0.15
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          releaseId: ${{ needs.release.outputs.release_id }}
          args: artifacts/*


================================================
FILE: .gitignore
================================================
*~
*.o
*.d
*.map
.deps
.gdb_history
.libs
.stamp
.unpacked
Makefile
Makefile.in
aclocal.m4
ar-lib
autom4te.cache/*
aux/
compile
config.*
configure
depcomp
install-sh
libtool
ltmain.sh
missing
stamp-h1
TAGS
ID
GPATH
GRTAGS
GSYMS
GTAGS


================================================
FILE: ChangeLog.md
================================================
Change Log
==========

All notable changes to the project are documented in this file.


[v2.15][] - 2021-12-20
----------------------

### Changes
- CI status badge now to points to GitHub Actions, no more Travis-CI
- Silence some developer debug messages
- Always skip `.` and `..` in FTP listings
- Internal refactoring and code cleanup

### Fixes
- Fix mdoc warning, found by lintian
- Fix regression introduced in v2.14, server directory name shown in FTP
  listings instead of `.`, e.g. `MLST .` showed the directory name 
- Fix #36: for real this time, now also with a test case to verify
- Fix #38: duplicate entries in FTP listings, regression in v2.14.
  Caused by (initially unintentional) removal of sorted listings, where
  directories prior to v2.14 were listed first.  This change, albeit an
  accident, actually helped clean up the code base and speed up replies


[v2.14][] - 2021-12-11
----------------------

### Changes
- Add support for `-o pasv_addr=ADDR` command line argument to override
- Add support for `-p PIDFILE` command line argument
  the address passed to the client in passive mode, useful for some
  types of NAT setup
- Add support for new libite (-lite) library header namespace
- Restored .tar.gz release archives
- Replaced Travis-CI with GitHub Actions

### Fixes
- Issue #36: MLST command without any argument blocks
- Fix memory leak in MLST/MLSD, only affects no-MMU systems where the
  kernel cannot free memory of processes on exit


[v2.13][] - 2020-06-30
----------------------

### Changes
- Unit test framework in place, with regression test for issue #31

### Fixes
- Issue #31: Socket leak in daemon accept() handling causing "Too many
  open files".  Effectively causing denial of service
- Minor memory leak fixed, only allocated once at startup.  Affects
  only non-MMU systems


[v2.12][] - 2020-05-25
----------------------

### Changes
- Use common log message format and log level when user enters an
  invalid path.  This unfortunately affects changes introduced in
  [v2.11][] to increase logging at default log level.

### Fixes
- Issue #30: When entering an invalid directory with the FTP command CWD,
  a NULL ptr was deref. in a DBG() message even though the log level is
  set to a value lower than `LOG_DEBUG`.  This caused uftpd to crash
  and cause denial of service.  Depending on the init/inetd system used
  this could be permanent.


[v2.11][] - 2020-01-05
----------------------

### Changes
- Increased logging at default log level.  Now users logging in,
  downloading, uploading, directory creation/removal is logged by
  default.  Start with `-l error` to silence uftpd again

### Fixes
- Fix buffer overflow in FTP PORT parser, reported by Aaron Esau
- Fix TFTP/FTP directory traversal regression , reported by Aaron Esau
- Fix potential DOS through non-busy loop and segfault, by Aaron Esau
- Fix potential segfault through empty FTP password, by Aaron Esau
- Fix potential segfault through FTP PORT command, by Aaron Esau


[v2.10][] - 2019-08-15
----------------------

### Changes
- Issue #25: Add support for TFTP write support (WRQ)
- Slightly improved debug messages.

### Fixes
- Minor fix to TFTP error codes, only use standardized codes, and
  code 0 + custom error message for everything else


[v2.9][] - 2019-07-29
---------------------

### Changes
- Reduced log level for "Invalid path" and "Failed realpath()" syslog
  messages.  Only relevant when debugging.  For use on the Internet it
  will otherwise cause an excessive amount of logs due to GXHLGSL.txt
- Debian packaging fixes and updates:
  - Reverts `-o writable`, due to fixing issue #22
  - Fixes failing `dpkg -P uftpd` due to bug in postrm script

### Fixes
- Issue #21: Check for `pkg-config` before looking for deps.
- Issue #22: Check FTP root security *after* having dropped privs.
  This means no longer having to run with `-o writable` by default
- Issue #23: FTP command `CWD /` does not work, affects all clients.
  This is a regression introduced in v2.8 while fixing #18


[v2.8][] - 2019-05-28
---------------------

### Changes
- The FTP command processor now always converts all inbound commands
  to uppercase to handle clients sending commands in lowercase
- Any arguments to the FTP `LIST` command are now ignored
- Improved user feedback on bad FTP root error message

### Fixes
- Fix #18: KDE Dolphin, FTP client interop problems.


[v2.7][] - 2019-03-03
---------------------

### Changes
- Documentation updates, commands added in v2.5 and `writable` opt
- Require libuEv v2.2, or later

### Fixes
- Issue #17: Issues with relative FTP root when running unprivileged


[v2.6][] - 2018-07-03
---------------------

Bug fix release.

### Fixes
- Issue #16: 100% CPU when client session exits
- Add missing include file for `gettimeofday()`
- Flush stdout logging when running in the foreground


[v2.5][] - 2018-06-06
---------------------

The VLC Android app release.

### Changes
- Support for `ABOR` FTP command, issue #14
- Support for `REST` FTP command, issue #13
- Support for `EPSV` and `EPSV ALL` FTP commands, issue #11
- Basic support for `MLST` and `MLSD` FTP commands to provide support
  for the VLC android app., issue #9 and #12
- Add `OPTS MLST <ARG>` to let client manage order of facts listed
  in `MLST` and `MLSD` calls
- Add `CDUP` FTP convenience command, alias to `CWD ..`
- Add `DELE` FTP command to delete files
- Add `MKD` and `RMD` FTP commands to create and remove directories
- Refactor `LIST`, `RETR`, `STOR` and `PASV` FTP commands for speed

### Fixes
- Really fix 100% CPU problem, issue #9.  Multiple failure modes in
  libuEv and improper handling of `waitpid()` in event loop callback
- Use libuEv callback also for `PASV` FTP connections
- Fix `NLST` + `LIST` line endings, must be \r\n


[v2.4][] - 2017-09-03
---------------------

Bug fix release.

### Changes
- Handle non-chrooted use-cases better, ensure CWD starts with /
- Increased default inactivity timer: 20 sec --> 180 sec
- Ensure FTP `PASV` and `PORT` sockets are set non-blocking to prevent
  blocking the event loop
- [README.md][] updates, add usage section and improve build + install

### Fixes
- Fix 100% CPU issue.  Triggered sometimes when a user issued `CWD ..`


[v2.3][] - 2017-03-22
---------------------

Bug fix release.

### Changes
- Add support for `MDTM`, modify time, some clients rely this
- Add support for correct `SIZE` when in ASCII mode
- Add basic code of conduct to project
- Add contributing guidelines, automatically referenced by GitHub
  when filing a bug report or pull request

### Fixes
- Fix 100% CPU bug caused by `RETR` of non-regular file or directory
- Fix segfault on missing FTP home
- Fix ordering issue in fallback FTP user handling, introduced in v2.2
- Fix error message on `CWD` to non-directory
- Fix `.deb` generation and debconf installation/reconfigure issues


[v2.2][] - 2017-03-14
---------------------

### Changes
- Sort directories first in FTP `LIST` command
- Make sure to exit all lingering FTP sessions on exit
- Logging: reduced verbosity of common FTP commands
- Logging: show client address on failed file retrieval
- Full Debian/Ubuntu `.deb` build support, including debconf,
  asking user what services (FTP and/or TFTP) to run.
- Verify FTP/TFTP root directory is not writable by default
- New option to allow writable FTP/TFTP root, disabled by default

### Fixes
- Fix FTP directory listings, was off-by-one, one entry missing
- Issue #7: Spelling error in `README.md`
- Issue #8: Install missing symlinks for `in.ftpd.8` and `in.tftpd.8`


[v2.1][] - 2016-06-05
---------------------

### Changes
- Remove GIT submodules for libuEv and libite, these two libraries
  are now required to be installed separately.
- The output from `uftpd -v` now only shows the version.


[v2.0.2][] - 2016-02-02
-----------------------

Minor fix release.

### Fixes
- Distribution build fixes for companion libraries
- Missing critical files in uftpd distribution


[v2.0.1][] - 2016-02-02
-----------------------

Minor fix release.

### Changes
- Upgrade to [libite][] v1.4.2 (GCC 6 bug fixes)

### Fixes
- IPv6 address conversion error, found by GCC 6
- Make install of symlinks for `in.tftpd` & `in.ftpd` idempotent. Check
  any existing `in.ftpd` and `in.tftpd` symlinks before bugging out.
  Fixes problem of uftpd install failing on already existing symlinks.


[v2.0][] - 2016-01-22
---------------------

Sleek, smart, simple ... UNIX

### Changes
- Greatly simplified command line syntax
- Run inetd services by calling `in.ftpd` and `in.tftpd` symlinks
- Migrate to GNU configure and build system
- Update and simplify man page
- Build statically against bundled versions of libite (LITE) and libuEv
- Update bundled libuEv to v1.3.0
- Update bundled libite to v1.4.1

### Fixes
- Do not allow VERSION to be overloaded by build system
- Do not enforce any optimization in Makefile, this is up to the user
- Minor fixes to redundant error messages when running as a regular user


[v1.9.1][] - 2015-09-27
-----------------------

Minor fix release.

### Changes
- Upgrade to [libuEv][] v1.2.3 (bug fixes)
- Upgrade to [libite][] v1.1.1 (bug fixes)
- Add support for linking against external libuEv and libite

### Fixes
- Misc. README updates
- Check if libite or libuEv are missing as submodules


[v1.9][] - 2015-07-23
---------------------

Bug fix release.  FTP and TFTP sessions can now run fully in parallel,
independent of each other.  Also improved compatibility with Firefox
built-in FTP client and wget.

### Changes
- Upgrade to [libuEv][] v1.2.1+ for improved error handling and a much
  cleaner API.
- Major refactor of both FTP and TFTP servers to use libuEv better.
- Move to use [libite][] v1.0.0 for `strlcpy()`, `strlcat()`, `pidfile()`
  and more.
- Add proper session timeout to TFTP, like what FTP already has.
- Add support for `NLST` FTP command, needed for multiple get operations.
  This fixes issue #2, thanks to @oz123 on GitHub for pointing this out!
- Add support for `FEAT` and `HELP` FTP commands used by some clients.

### Fixes
- Fix issue #3: do not sleep 2 sec before exiting.  Simply forward the
  `SIGTERM` to any FTP/TFTP session in progress, yield the CPU to let
  the child sessions handle the signal, and then exit.  Much quicker!
- Fix issue #4: due to an ordering bug between the main process calling
  `daemon()` and `sig_init()`, we never got the `SIGCHILD` to be able to
  reap any exiting FTP/TFTP sessions.  This resulted in zombies(!) when
  *not* being called as `uftpd -n`
- Fix issue #5: `LIST` and `NLST` ignores path argument sent by client.
- Fix issue #6: FTP clients not detecting session timeout.  Caused by
  uftpd not performing a proper `shutdown()` on the client socket(s)
  before `close()`.
- Fix problem with [libuEv][] not being properly cleaned on `distclean`.
- Fix problem with uftpd not exiting client session properly when client
  simply closes the connection.


[v1.8][] - 2015-02-02
---------------------

### Changes
- Updated [README.md][]
- Add [TODO.md][]
- Add [CHANGELOG.md][], attempt to align with <http://keepachangelog.com>
- From now on [Travis-CI][] only runs when pushing to the dev branch,
  so all new development must be done there.
- Upgrade to [libuEv][] v1.0.4

### Fixes
- Fix insecure `chroot()` reported in [Coverity Scan][] CID #54523.
- Minor cleanup fixes.


[v1.7][] - 2014-12-21
---------------------

The TFTP Blocksize Negotiation release.

### Changes
- Support for [RFC 2348][], TFTP blocksize negotiation
- Support for custom server directory, instead of FTP user's `$HOME`
- Log to `stderr` when running in foreground and debug is enabled


[v1.6][] - 2014-09-12
---------------------

Fix missing [libuEv][] directory content generated by <kbd>make dist</kbd>
in [v1.3][], [v1.4][], and [v1.5][].

### Fixes
- Since the introduction of the event library [libuEv][] the <kbd>make
  dist</kbd> target has failed to include the libuev sub-directory.
  This is due to the `git archive` command unfortunately not supporting
  git sub-modules.


[v1.5][] - 2014-09-12 [YANKED]
------------------------------

Major fix release, lots of issues reported by [Coverity Scan][] fixed.
For details, see <https://scan.coverity.com/projects/2947>

**Note:** This release has been *yanked* from distribution due to the
tarball (generated by the <kbd>make dist</kbd>) missing the required
libuEv library.  Instead, use [v1.6][] or later, where this is fixed, or
roll your own build of this release from the GIT source tree.

### Changes
- Add support for [Travis-CI][], continuous integration with GitHub
- Add support for [Coverity Scan][], the best static code analyzer,
  integrated with [Travis-CI][] -- scan runs for each push to master

### Fixes
- Fix nasty invalid `sizeof()` argument to `recv()` causing uftpd to
  only read 4/8 bytes (32/64 bit arch) at a time from the FTP socket.
  This should greatly reduce CPU utilization and improve xfer speeds.
  Found by [Coverity Scan][].
- Fix minor resource leak in `ftp_session()` when `getsockname()` or
  `getpeername()` fail.  Minor fix because the session exits and the OS
  usually frees resources at that point, unless you're using uClinux.
  Found by [Coverity Scan][].
- Various fixes for unchecked API return values, prevents propagation of
  errors.  Also, make sure to clear input data before calling API's.
  Found by [Coverity Scan][].
- Fix oversight in checking for invalid/missing FTP username.
  Found by [Coverity Scan][].
- Fix potential attack vector.  Make sure to always store a NUL string
  terminator in all received FTP commands so the parser does not go out
  of bounds. Found by [Coverity Scan][].
- Fix parallel build problems in `Makefile`.


[v1.4][] - 2014-09-04 [YANKED]
------------------------------

**Note:** This release has been *yanked* from distribution due to the
tarball (generated by the <kbd>make dist</kbd>) missing the required
libuEv library.  Instead, use [v1.6][] or later, where this is fixed, or
roll your own build of this release from the GIT source tree.

### Changes
- Update documentation, both built-in usage text and man page.

### Fixes
- Fix bug in inetd.conf installed by .deb package for TFTP service.
  Inetd forked off a new TFTP session for each connection attempt.


[v1.3][] - 2014-09-04 [YANKED]
------------------------------

Added support for TFTP, [RFC 1350][].  Integration of the asynchronous
event library [libuEv][], to serialize all events.  Massive refactoring.

**Note:** This release has been *yanked* from distribution due to the
tarball (generated by the <kbd>make dist</kbd>) missing the required
libuEv library.  Instead, use [v1.6][] or later, where this is fixed, or
roll your own build of this release from the GIT source tree.

### Changes
- Incompatible changes to the command line arguments, compared to v1.2!
- Add libuEv as a GIT submodule, handles signals, timers, and all I/O.
- Refactor all signal handling, timers, and socket `poll()` calls to
  use libuEv instead.  Much cleaner and maintainable code as a result.
- Clarify copyright claims, not much remains of the original [FtpServer][]
  code, by [Xu Wang][].


[v1.2][] - 2014-05-19
---------------------

### Changes
- Add support for logging to stdout as well as syslog.

### Fixes
- Fix embarrassing problem with listing big/average sized directories.


[v1.1][] - 2014-05-04
---------------------

Haunted zombie (¬°-°)¬ release.

### Changes
- Add strict FTP session inactivity timer, 20 sec.
- Change some logs to informational, only seen in verbose `-V` mode.
- Revise .deb package slightly and add support for creating an FTP user
  and group on the system.  This is used to both find the default FTP
  home directory, to serve files from, and also the UID/GID to drop to
  when being started as root.

### Fixes
- Fix zombie problem.  Forked off FTP sessions did not exit properly and
  were not `wait()`'ed for properly, so uftpd left a zombie processes
  lingering after each session.
- Fix ordering bug in security mechanism "drop privs"


[v1.0][] - 2014-05-04
---------------------

First official uftpd release! :-)

### Changes
- Forked from [FtpServer][], by [Xu Wang][].
- Add permissive [ISC license][].
- Massive refactor, code cleanup/renaming and "UNIX'ification":
  - Add actual command line parser.
  - Cleanup all log messages.
  - Reindent to use Linux KNF.
  - Use system's FTP user to figure out FTP home directory, with
    built-in fallback to `/srv/ftp`
  - Use system's `ftp/tcp` port from `/etc/services`.
  - Chroot to FTP home directory.
  - Support for dropping privileges if a valid FTP user exists.
  - Use `fork()` instead of pthreads for FTP client sessions.
  - Daemonize uftpd by default, detach from controlling terminal and
    reparent to PID 1 (init).
  - Add support for running as an `inetd` service.
  - Add wrapper for `syslog()` instead of using `stdout/stderr`.
  - Add basic `uftpd.8` man page.
- Add OpenBSD `strlcat()` and `strlcpy()` safe string functions.
- Add support for NOOP (keepalive sent by some clients).
- Add support for SIZE.
- Add support for TYPE, at least `IMAGE/BINARY`.
- Add basic dependency handling to Makefile.
- Add support for building Debian .deb packages.

### Fixes
- Handle "walking up to parent" attacks in several FTP functions.
- Fix memory leaks in `recv_mesg()` caused by dangerous homegrown string
  functions.  Replaced with safer OpenBSD variants.
- Fix absolute paths in FTP `LIST` command.
- Fix Firefox FTP mode `LIST` compatibility issue.
- Fix "bare linefeeds" warning from certain FTP clients in ASCII mode.
  Lines must end in the old `\r\n` format, rather than UNIX `\n`.


[UNRELEASED]:    https://github.com/troglobit/uftpd/compare/v2.15...HEAD
[v2.15]:         https://github.com/troglobit/uftpd/compare/v2.14...v2.15
[v2.14]:         https://github.com/troglobit/uftpd/compare/v2.13...v2.14
[v2.13]:         https://github.com/troglobit/uftpd/compare/v2.12...v2.13
[v2.12]:         https://github.com/troglobit/uftpd/compare/v2.11...v2.12
[v2.11]:         https://github.com/troglobit/uftpd/compare/v2.10...v2.11
[v2.10]:         https://github.com/troglobit/uftpd/compare/v2.9...v2.10
[v2.9]:          https://github.com/troglobit/uftpd/compare/v2.8...v2.9
[v2.8]:          https://github.com/troglobit/uftpd/compare/v2.7...v2.8
[v2.7]:          https://github.com/troglobit/uftpd/compare/v2.6...v2.7
[v2.6]:          https://github.com/troglobit/uftpd/compare/v2.5...v2.6
[v2.5]:          https://github.com/troglobit/uftpd/compare/v2.4...v2.5
[v2.4]:          https://github.com/troglobit/uftpd/compare/v2.3...v2.4
[v2.3]:          https://github.com/troglobit/uftpd/compare/v2.2...v2.3
[v2.2]:          https://github.com/troglobit/uftpd/compare/v2.1...v2.2
[v2.1]:          https://github.com/troglobit/uftpd/compare/v2.0.2...v2.1
[v2.0.2]:        https://github.com/troglobit/uftpd/compare/v2.0.1...v2.0.2
[v2.0.1]:        https://github.com/troglobit/uftpd/compare/v2.0...v2.0.1
[v2.0]:          https://github.com/troglobit/uftpd/compare/v1.9.1...v2.0
[v1.9.1]:        https://github.com/troglobit/uftpd/compare/v1.9...v1.9.1
[v1.9]:          https://github.com/troglobit/uftpd/compare/v1.8...v1.9
[v1.8]:          https://github.com/troglobit/uftpd/compare/v1.7...v1.8
[v1.7]:          https://github.com/troglobit/uftpd/compare/v1.6...v1.7
[v1.6]:          https://github.com/troglobit/uftpd/compare/v1.5...v1.6
[v1.5]:          https://github.com/troglobit/uftpd/compare/v1.4...v1.5
[v1.4]:          https://github.com/troglobit/uftpd/compare/v1.3...v1.4
[v1.3]:          https://github.com/troglobit/uftpd/compare/v1.2...v1.3
[v1.2]:          https://github.com/troglobit/uftpd/compare/v1.1...v1.2
[v1.1]:          https://github.com/troglobit/uftpd/compare/v1.0...v1.1
[v1.0]:          https://github.com/troglobit/uftpd/compare/v0.1...v1.1
[libuEv]:        https://github.com/troglobit/libuev
[libite]:        https://github.com/troglobit/libite
[ISC license]:   http://en.wikipedia.org/wiki/ISC_license
[RFC 1350]:      http://tools.ietf.org/html/rfc1350
[RFC 2348]:      http://tools.ietf.org/html/rfc2348
[Xu Wang]:       https://github.com/xu-wang11/
[FtpServer]:     https://github.com/xu-wang11/FtpServer
[Travis-CI]:     https://travis-ci.org/troglobit/uftpd
[Coverity Scan]: https://scan.coverity.com/projects/2947
[TODO.md]:       https://github.com/troglobit/uftpd/blob/master/docs/TODO.md
[README.md]:     https://github.com/troglobit/uftpd/blob/master/README.md
[CHANGELOG.md]:  https://github.com/troglobit/uftpd/blob/master/CHANGELOG.md


================================================
FILE: LICENSE
================================================
Copyright (C) 2014-2021  Joachim Wiberg <troglobit@gmail.com>

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.


================================================
FILE: Makefile.am
================================================
SUBDIRS            = src man test
doc_DATA           = README.md LICENSE ChangeLog.md
EXTRA_DIST         = README.md LICENSE ChangeLog.md

## Check if tagged in git
release-hook:
	if [ ! `git tag -l v$(PACKAGE_VERSION) | grep $(PACKAGE_VERSION)` ]; then	\
		echo;									\
		printf "\e[1m\e[41mCannot find release tag $(PACKAGE_VERSION)\e[0m\n";	\
		printf "\e[1m\e[5mDo release anyway?\e[0m "; read yorn;			\
		if [ "$$yorn" != "y" -a "$$yorn" != "Y" ]; then				\
			printf "OK, aborting release.\n";				\
			exit 1;								\
		fi;									\
		echo;									\
	else										\
		echo;									\
		printf "\e[1m\e[42mFound GIT release tag $(PACKAGE_VERSION)\e[0m\n";	\
		printf "\e[1m\e[44m>>Remember to push tags!\e[0m\n";			\
		echo;									\
	fi

## Generate .deb package
package build-deb:
	@debuild -uc -us -B --lintian-opts --profile debian -i -I --show-overrides

## Target to run when building a release
release: release-hook distcheck
	@for file in $(DIST_ARCHIVES); do		\
		md5sum    $$file > ../$$file.md5;	\
		sha256sum $$file > ../$$file.sha256;	\
	done
	@mv $(DIST_ARCHIVES) ../
	@echo
	@echo "Resulting release files:"
	@echo "================================================================="
	@for file in $(DIST_ARCHIVES); do						\
		printf "%-32s Distribution tarball\n" $$file;				\
		printf "%-32s " $$file.md5;    cat ../$$file.md5    | cut -f1 -d' ';	\
		printf "%-32s " $$file.sha256; cat ../$$file.sha256 | cut -f1 -d' ';	\
	done


================================================
FILE: README.md
================================================
No Nonsense FTP/TFTP Server
===========================
[![License Badge][]][License] [![GitHub Status][]][GitHub] [![Coverity Status][]][Coverity Scan]

uftpd is a UNIX daemon with sane built-in defaults.  It just works.


Features
--------

* FTP and/or TFTP
* No complex configuration file
* Runs from standard UNIX inetd, or standalone
* Uses `ftp` user's `$HOME`, from `/etc/passwd`, or custom path
* Uses `ftp/tcp` and `tftp/udp` from `/etc/services`, or custom ports
* Privilege separation, drops root privileges having bound to ports
* Possible to use symlinks outside of the FTP home directory
* Possible to have group writable FTP home directory


Usage
-----

```
uftpd [-hnsv] [-l LEVEL] [-o OPTS] [PATH]

  -h         Show this help text
  -l LEVEL   Set log level: none, err, notice (default), info, debug
  -n         Run in foreground, do not detach from controlling terminal
  -o OPT     Options:
                      ftp=PORT
                      tftp=PORT
                      pasv_addr=ADDR
                      writable
  -s         Use syslog, even if running in foreground, default w/o -n
  -v         Show program version

The optional 'PATH' defaults to the $HOME of the /etc/passwd user 'ftp'
Bug report address: https://github.com/troglobit/uftpd/issues
```

To start uftpd in the background as an FTP/TFTP server:

    uftpd

If the `ftp` user does not exist on your system, `uftpd` defaults to
serve files from the `/srv/ftp` directory.  To serve another directory,
simply append that directory to the argument list.

Use `sudo`, or set `CAP_NET_BIND_SERVICE` capabilities, on `uftpd` to
allow regular users to start `uftpd` on privileged (standard) ports,
i.e. `< 1024`:

    sudo setcap cap_net_bind_service+ep uftpd

To change port on either FTP or TFTP, use:

    uftpd -o ftp=PORT,tftp=PORT

Set `PORT` to zero (0) to disable either service.

New sessions are droppbed by default if uftpd detects the FTP root is
writable.  To allow writable FTP root:

    uftpd -o writable PATH

> **Note:** since v2.11 uftpd logs a lot more events by default.  Set up
> your syslogd to redirect `LOG_FTP` to a separate log file, or reduce
> the log level of uftpd using `-l error` to only log errors and higher.


Running from inetd
------------------

Rarely used services like FTP/TFTP are good candidates to run from the
Internet super server, inetd.  On Debian and Ubuntu based distributions
we recommend `openbsd-inetd`.

Use the following two lines in `/etc/inetd.conf`, notice how `in.ftpd`
and `in.tftpd` are symlinks to the `uftpd` binary:

    ftp     stream  tcp nowait  root    /usr/sbin/in.ftpd
    tftp    dgram   udp wait    root    /usr/sbin/in.tftpd

Remember to activate your changes to inetd by reloading the service or
sending `SIGHUP` to it.  Another inetd server may use different syntax.
Like the inetd that comes built-in to [Finit][], in `/etc/finit.conf`:

    inetd ftp/tcp   nowait /usr/sbin/in.ftpd  -- The uftpd FTP server
    inetd tftp/udp    wait /usr/sbin/in.tfptd -- The uftpd TFTP server


Caveat
------

uftpd is primarily not targeted at secure installations, it is targeted
at users in need of a *simple* FTP/TFTP server.

uftpd allows symlinks outside the FTP root, as well as a group writable
FTP home directory &mdash; user-friendly features that potentially can
cause security breaches, but also very useful for people who just want
their FTP server to work.  A lot of care has been taken, however, to
lock down and secure uftpd by default.


Build & Install
---------------

### Debian/Ubuntu

    curl -sS https://deb.troglobit.com/pubkey.gpg | sudo apt-key add -
    echo "deb [arch=amd64] https://deb.troglobit.com/debian stable main" | sudo tee /etc/apt/sources.list.d/troglobit.list
    sudo apt-get update && sudo apt-get install uftpd

### Building from Source

`uftpd` depends on two other projects to build from source, [libuEv][]
and [lite][].  See their respective README for details, there should be
no real surprises, both use the familiar configure, make, make install.

To find the two libraries uftpd depends on `pkg-config`.  The package
name for your Linux distribution varies, on Debian/Ubuntu systems:

```shell
user@example:~/> sudo apt install pkg-config
```

uftpd, as well as its dependencies, can be built as `.deb` packages on
Debian or Ubuntu based distributions.  Download and install each of the
dependencies, and then run

    ./autogen.sh      <--- Only needed if using GIT sources
    ./configure
    make package

The `.deb` package takes care of setting up `/etc/inetd.conf`, create an
`ftp` user and an `/srv/ftp` home directory with write permissions for
all members of the `users` group.

If you are using a different Linux or UNIX distribution, check the
output from `./configure --help`, followed by `make all install`.
For instance, building on [Alpine Linux](https://alpinelinux.org/):

    PKG_CONFIG_LIBDIR=/usr/local/lib/pkgconfig ./configure \
	    --prefix=/usr --localstatedir=/var --sysconfdir=/etc

Provided the library dependencies were installed in `/usr/local/`.  This
`PKG_CONFIG_LIBDIR` trick may be needed on other GNU/Linux, or UNIX,
distributions as well.


Origin & References
-------------------

uftpd was originally based on [FtpServer][] by [Xu Wang][], but is now a
complete rewrite with TFTP support by [Joachim Wiberg][], maintained at
[GitHub][home].


[Joachim Wiberg]: http://troglobit.com
[the FTP]:         http://ftp.troglobit.com/uftpd/
[Xu Wang]:         https://github.com/xu-wang11/
[FtpServer]:       https://github.com/xu-wang11/FtpServer
[home]:            https://github.com/troglobit/uftpd
[Finit]:           https://github.com/troglobit/finit
[lite]:            https://github.com/troglobit/libite
[libuEv]:          https://github.com/troglobit/libuev
[License]:         https://en.wikipedia.org/wiki/ISC_license
[License Badge]:   https://img.shields.io/badge/License-ISC-blue.svg
[GitHub]:          https://github.com/troglobit/uftpd/actions/workflows/build.yml/
[GitHub Status]:   https://github.com/troglobit/uftpd/actions/workflows/build.yml/badge.svg
[Coverity Scan]:   https://scan.coverity.com/projects/2947
[Coverity Status]: https://scan.coverity.com/projects/2947/badge.svg


================================================
FILE: autogen.sh
================================================
#!/bin/sh

autoreconf -W portability -visfm


================================================
FILE: configure.ac
================================================
AC_INIT([uftpd], [2.15], [https://github.com/troglobit/uftpd/issues], [],
	[https://troglobit.com/projects/uftpd/])
AC_CONFIG_AUX_DIR(aux)
AM_INIT_AUTOMAKE([1.11 foreign dist-xz])
AM_SILENT_RULES([yes])

AC_CONFIG_SRCDIR([src/uftpd.c])
AC_CONFIG_HEADERS([config.h])
AC_CONFIG_FILES([Makefile src/Makefile man/Makefile test/Makefile])

AC_PROG_CC
AC_PROG_LN_S
AC_PROG_INSTALL

# Configuration.
AC_CHECK_HEADERS(sys/time.h)
AC_CHECK_FUNCS(strstr getopt getsubopt gettimeofday)

# Check for uint[8,16,32]_t
AC_TYPE_UINT8_T
AC_TYPE_UINT16_T
AC_TYPE_UINT32_T

# Check for pkg-config first, warn if it's not installed
PKG_PROG_PKG_CONFIG

# Check for required libraries
PKG_CHECK_MODULES([uev],  [libuev >= 2.2.0])
PKG_CHECK_MODULES([lite], [libite >= 1.5.0])

AC_OUTPUT


================================================
FILE: debian/.gitignore
================================================
autoreconf.*
debhelper-build-stamp
files
uftpd.debhelper.log
uftpd.post*
uftpd.pre*
uftpd.substvars
uftpd/


================================================
FILE: debian/README.Debian
================================================
uftpd for Debian/Ubuntu
-----------------------

uftpd is a true UNIX TFTP/FTP daemon, it serves files, and nothing more.
It runs from inetd on ports specified in /etc/services, serving files
from the ftp user's $HOME, /src/ftp -- it just works.

 -- Joachim Wiberg <troglobit@gmail.com>, Sat, 11 Dec 2021 08:46:52 +0100


================================================
FILE: debian/changelog
================================================
uftpd (2.15) stable; urgency=medium

  * Silence some developer debug messages
  * Always skip `.` and `..` in FTP listings
  * Fix mdoc warning, found by lintian
  * Fix regression introduced in v2.14, server directory name shown in FTP
    listings instead of `.`, e.g. `MLST .` showed the directory name 
  * Fix #36: for real this time, now also with a test case to verify
  * Fix #38: duplicate entries in FTP listings, regression in v2.14.
    Caused by (initially unintentional) removal of sorted listings, where
    directories prior to v2.14 were listed first.  This change, albeit an
    accident, actually helped clean up the code base and speed up replies

 -- Joachim Wiberg <troglobit@gmail.com>  Mon, 20 Dec 2021 06:15:08 +0100

uftpd (2.14) stable; urgency=medium

  * Add support for `-o pasv_addr=ADDR` command line argument to override
  * Add support for `-p PIDFILE` command line argument
    the address passed to the client in passive mode, useful for some
    types of NAT setup
  * Fix issue #36: MLST command without any argument blocks
  * Fix memory leak in MLST/MLSD, only affects no-MMU systems where the
    kernel cannot free memory of processes on exit

 -- Joachim Wiberg <troglobit@gmail.com>  Sat, 11 Dec 2021 11:27:57 +0100

uftpd (2.13) unstable; urgency=medium

  * Fix issue #31: Socket leak in daemon accept(), causing denial of
    service in standalone daemon setups.  Does not affect .deb install.
  * Fix minor memory leak, only affects non-MMU systems.

 -- Joachim Nilsson <troglobit@gmail.com>  Tue, 30 Jun 2020 23:36:35 +0200

uftpd (2.12) stable; urgency=medium

  * Fix issue #30: uftpd crashes when an invalid CWD is entered
  * Use common log message format and log level for all path refs.

 -- Joachim Nilsson <troglobit@gmail.com>  Mon, 25 May 2020 18:08:32 +0200

uftpd (2.11) unstable; urgency=medium

  * Increased logging at default log level.  Now all relevant interaction
    is logged.  See the man page for how to adjust.
  * Fix buffer overflow in FTP PORT parser
  * Fix TFTP/FTP directory traversal regression
  * Fix potential DOS through non-busy loop and segfault
  * Fix potential segfault through empty FTP password
  * Fix potential segfault through FTP PORT command

 -- Joachim Nilsson <troglobit@gmail.com>  Sun, 05 Jan 2020 08:49:56 +0100

uftpd (2.10) unstable; urgency=medium

  * Add support for TFTP WRQ, i.e. for clients sending files to server
  * Fix invalid TFTP error codes, now uses custom error string to code 0
  * Slightly improved debug messages

 -- Joachim Nilsson <troglobit@gmail.com>  Thu, 15 Aug 2019 08:59:35 +0200

uftpd (2.9) unstable; urgency=medium

  * Check FTP root security after dropping privileges, issue #22
  * Revert insecure default: "writable FTP root", introduced in v2.8
  * Revert part of issue #18 to fix issue #23; "CWD /" doesn't work
  * Update debian packaging to policy 4.3.0
  * Fix failing postrm script, causing dpkg -P uftpd to fail hard
  * Fix spelling errors found by Lintian
  * Fix package description, more formal and less personal, thanks Lintian

 -- Joachim Nilsson <troglobit@gmail.com>  Mon, 29 Jul 2019 10:52:49 +0200

uftpd (2.8) unstable; urgency=medium

  * Fix off-by-one regression introduced in v2.5
  * Convert all commands from user to uppercase for processing
  * Skip any and *all* FTP LIST options
  * Enable users group writable FTP root in /etc/inetd.conf

 -- Joachim Nilsson <troglobit@gmail.com>  Tue, 28 May 2019 06:22:18 +0200

uftpd (2.7) unstable; urgency=medium

  * Bug fix release
  * Fix running uftpd as unprivileged user using a relative FTP root

 -- Joachim Nilsson <troglobit@gmail.com>  Sun, 03 Mar 2019 11:39:03 +0100

uftpd (2.6) unstable; urgency=medium

  * Bug fix release
  * Really fix 100% CPU issue, take two.  Some clients managed to trigger
    a bug caused by calling `uev_exit()` twice on client session exit

 -- Joachim Nilsson <troglobit@gmail.com>  Tue, 03 Jul 2018 17:14:00 +0200

uftpd (2.5) unstable; urgency=critical

  * Really fix 100% CPU issue
  * Fix line endings for NLST and LIST FTP commands
  * Add support for EPSV, MLSD, ABOR, and REST FTP commands, required
    for VLC Android app
  * Add support for CDUP, DELE, MKD, RMD, MLST, and OPTS MLST
  * Refactor LIST, RETR, STOR and PASV FTP commands for speed

 -- Joachim Nilsson <troglobit@gmail.com>  Sat, 19 May 2018 13:35:01 +0200

uftpd (2.4-1) unstable; urgency=medium

  * New upstream release, fixes 100% CPU issue reported by some users.

 -- Joachim Nilsson <troglobit@gmail.com>  Sun, 03 Sep 2017 12:38:38 +0200

uftpd (2.3-1) stable; urgency=low

  * New upstream release, fixes issue with lingering inetd FTP processes
    using up 100% CPU.

 -- Joachim Nilsson <troglobit@gmail.com>  Wed, 22 Mar 2017 07:56:00 +0100

uftpd (2.2-4) stable; urgency=low

  * Fix dpkg-reconfigure support.  When disabling either TFTP/FTP
    the disabled service was not properly disabled in /etc/inetd.conf

 -- Joachim Nilsson <troglobit@gmail.com>  Sun, 14 Mar 2017 22:05:00 +0100

uftpd (2.2-3) stable; urgency=low

  * Add debconf support
  * Change default group and permissions of /srv/ftp to prevent any
    future "security" breaches by uploads to the FTP root directory.

    A user must now be member of the users group to share files over
    TFTP/FTP.  Simply add a user to 'users' and they can upload their
    files to /srv/ftp.  The TFTP/FTP server itself has no rights to
    write there.  Add an uploads/ sub-directory with write perms for
    the 'ftp' user if you want to enable anonymous uploads via FTP.

 -- Joachim Nilsson <troglobit@gmail.com>  Sun, 13 Mar 2017 19:56:00 +0100

uftpd (2.2-1) stable; urgency=low

  * New upstream release, v2.2

 -- Joachim Nilsson <troglobit@gmail.com>  Sun, 12 Mar 2017 17:36:00 +0100

uftpd (2.1-1) stable; urgency=low

  * New upstream release, v2.1
    - Removed built-in GIT submodules for libite and libuev
    - Updated README

 -- Joachim Nilsson <troglobit@gmail.com>  Sun,  5 Jun 2016 00:19:30 +0100

uftpd (2.0-1) stable; urgency=low

  * New upstream release, v2.0
    - Completely changed command line syntax
    - New binaries (symlinks) for inetd usage, in.tftpd and in.ftpd
    - Updated man page

 -- Joachim Nilsson <troglobit@gmail.com>  Sun, 22 Jan 2016 13:51:56 +0100

uftpd (2.0-rc1-1) unstable; urgency=low

  * New upstream release, v2.0-rc1
    - Completely changed command line syntax
    - New binaries (symlinks) for inetd usage

 -- Joachim Nilsson <troglobit@gmail.com>  Sun, 17 Jan 2016 23:16:58 +0100

uftpd (1.9.1-1) unstable; urgency=medium

  * Minor fix release
  * Added support for building with external libite and libuEv
  * Rebuild for Ubuntu 15.10

 -- Joachim Nilsson <troglobit@gmail.com>  Sun, 27 Sep 2015 10:14:31 +0200

uftpd (1.9-1) unstable; urgency=low

  * New upstream release:
    - Add support for NLST FTP command.
    - Fix problem with 2 seconds before exiting on SIGTERM/SIGINT.
    - Fix zombie problem when backgrounding.
    - Add support for automatically creating a PID file.
    - Fix problem with LIST (and NLST) ignoring path argument.
    - Fix problem with FTP client simply closing connection (no QUIT)

 -- Joachim Nilsson <troglobit@gmail.com>  Mon, 13 Jul 2015 01:25:26 +0200

uftpd (1.8-1) unstable; urgency=low

  * New upstream release:
    - Fix insecure chroot(), reported by Coverity Scan, CID #54523
    - Minor updates to README and a new CHANGELOG file

 -- Joachim Nilsson <troglobit@gmail.com>  Sun,  2 Feb 2015 06:45:06 +0100

uftpd (1.7-1) unstable; urgency=low

  * New upstream release:
    - Support for TFTP blocksize negotiation.
    - Support for custom FTP server directory.
    - Log to stderr when in foreground AND debug mode.

 -- Joachim Nilsson <troglobit@gmail.com>  Sun, 21 Dec 2014 19:35:29 +0100

uftpd (1.6-1) unstable; urgency=low

  * Repack of 1.5 due to missing content in new libuev subdirectory in
    distributed tarball.

 -- Joachim Nilsson <troglobit@gmail.com>  Fri, 12 Sep 2014 15:45:08 +0200

uftpd (1.5-1) unstable; urgency=low

  * New upstream release, minor bug fixes only, found by Coverity Scan.
  * Updates to README

 -- Joachim Nilsson <troglobit@gmail.com>  Fri, 12 Sep 2014 01:30:01 +0200

uftpd (1.4-1) unstable; urgency=low

  * New upstream release, bug fix TFTP start in /etc/inetd.conf
  * Updates to man page.

 -- Joachim Nilsson <troglobit@gmail.com>  Thu, 04 Sep 2014 22:16:22 +0200

uftpd (1.3-1) unstable; urgency=low

  * New upstream release, with TFTP support.
  * Incompatible change in command line options.

 -- Joachim Nilsson <troglobit@gmail.com>  Tue, 19 Aug 2014 23:27:28 +0200

uftpd (1.2-1) unstable; urgency=low

  * New upstream release.  Fixes problem with listing "big" directories.

 -- Joachim Nilsson <troglobit@gmail.com>  Mon, 19 May 2014 22:02:17 +0200

uftpd (1.1-1) unstable; urgency=low

  * New release.  Fixes problem with lingering zombie processes and an
    ordering bug preventing drop privs from working properly.

 -- Joachim Nilsson <troglobit@gmail.com>  Sun, 04 May 2014 23:37:24 +0200

uftpd (1.0-2) unstable; urgency=low

  * Add support for creating and removing the standard ftp user on
    installation and removal.  Use /srv/ftp as $HOME and make sure
    to not remove $HOME when removing uftpd.  This also means that
    the uftpd privsep mode gets its first testing.

 -- Joachim Nilsson <troglobit@gmail.com>  Sun, 04 May 2014 19:28:16 +0200

uftpd (1.0-1) unstable; urgency=low

  * Initial release.

 -- Joachim Nilsson <troglobit@gmail.com>  Sun,  4 May 2014 03:03:32 +1000



================================================
FILE: debian/compat
================================================
10


================================================
FILE: debian/config
================================================
#!/bin/sh

set -e
. /usr/share/debconf/confmodule

db_title uftpd

db_input critical uftpd/ftp || true
db_go

db_input critical uftpd/tftp || true
db_go



================================================
FILE: debian/control
================================================
Source: uftpd
Section: net
Priority: optional
Maintainer: Joachim Wiberg <troglobit@gmail.com>
Build-Depends: debhelper (>= 10)
Standards-Version: 4.3.0
Homepage: https://troglobit.com/projects/uftpd/

Package: uftpd
Architecture: any
Pre-Depends: adduser
Depends: openbsd-inetd | inet-superserver, debconf (>= 0.2.17), ${shlibs:Depends}, ${misc:Depends}
Provides: ftp-server
Conflicts: ftp-server, tftpd, tftpd-hpa
Description: No nonsense TFTP/FTP server
 uftpd is a small and simple TFTP and FTP server intended for LANs.  Its
 author runs it on the Internet, although this is not recommended.
 .
 uftpd is set up in a read-only configuration by default.  It has no
 users, except for anonymous, no configuration file, and is started
 on-demand by the UNIX inetd super server, neatly tcpwrapped for your
 safety.
 .
 Hardcore Internet users and anyone concerned about security should
 probably consider a separate TFTP server and for FTP look at one of:
 vsftpd, proftpd, or pure-ftpd.


================================================
FILE: debian/copyright
================================================

Copyright: (c) 2014-2021  Joachim Wiberg <troglobit@gmail.com>

License: ISC
 Permission to use, copy, modify, and/or distribute this software for any
 purpose with or without fee is hereby granted, provided that the above
 copyright notice and this permission notice appear in all copies.
 
 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.



================================================
FILE: debian/dirs
================================================
usr/share/man/man8
usr/sbin


================================================
FILE: debian/docs
================================================
README.md


================================================
FILE: debian/postinst
================================================
#!/bin/sh

set -e

[ "$1" = "configure" ] || exit 0

# Source debconf library.
. /usr/share/debconf/confmodule

FTPENTRY="ftp		stream	tcp	nowait	root	/usr/sbin/tcpd	in.ftpd"
TFTPENTRY="tftp		dgram	udp	wait	root	/usr/sbin/tcpd	in.tftpd"

if [ ! -f /etc/inetd.conf -a -d /etc/xinetd.d -a -x /usr/sbin/xinetd ]; then
	cat <<-TEXT
		-------------------------------------------------
		There is no configuration support for using uftpd
		under the control of xinetd.
		-------------------------------------------------
	TEXT
fi

if grep -q '[[:blank:]]/usr/sbin/uftpd.*' /etc/inetd.conf 2>/dev/null; then
	update-inetd --pattern '/usr/sbin/uftpd' --remove ".*ftp"
fi

update-inetd --group STANDARD --add "$FTPENTRY"
update-inetd --group STANDARD --add "$TFTPENTRY"

db_get uftpd/ftp
if [ "$RET" = "true" ]; then
    update-inetd --enable ftp
else
    update-inetd --disable ftp
fi

db_get uftpd/tftp
if [ "$RET" = "true" ]; then
    update-inetd --enable tftp
else
    update-inetd --disable tftp
fi

# Redirect errors from adduser since 1) user may exist already,
# 2) directory may exist and not be writable by user.  Ignore.
if ! grep -q "^ftp:" /etc/passwd; then
	addgroup --quiet --system ftp
	adduser --quiet --system --disabled-login --home /srv/ftp \
		--ingroup ftp ftp 2>/dev/null || true
fi

# Adjust for any previous server, users wanting to share files using
# TFTP/FTP should be in the users group
chown --changes ftp:users /srv/ftp
chmod --changes 0575      /srv/ftp


================================================
FILE: debian/postrm
================================================
#!/bin/sh
set -e

if [ "$1" = "purge" ]; then
	if command -v update-inetd >/dev/null 2>&1; then
		update-inetd --pattern 'uftpd' --remove ".*ftp"
		update-inetd --pattern 'in.ftpd' --remove ftp
		update-inetd --pattern 'in.tftpd' --remove tftp
	fi

	# Remove uftpd entries from db
	if [ -f /usr/share/debconf/confmodule ]; then
		. /usr/share/debconf/confmodule
		db_purge
	fi
fi

deluser --quiet --system ftp

exit 0


================================================
FILE: debian/prerm
================================================
#!/bin/sh

set -e

update-inetd --pattern 'in.ftpd' --multi --disable ftp
update-inetd --pattern 'in.tftpd' --multi --disable tftp


================================================
FILE: debian/rules
================================================
#!/usr/bin/make -f
# export DH_VERBOSE=1
export DEB_BUILD_MAINT_OPTIONS = hardening=+all
export DEB_BUILD_OPTIONS='parallel=1'

%:
	dh $@ --with autoreconf,systemd

override_dh_installchangelogs:
	dh_installchangelogs ChangeLog.md

# Remove LICENSE and ChangeLog.md per Debian Policy
override_dh_auto_install:
	dh_auto_install
	rm -v debian/uftpd/usr/share/doc/uftpd/LICENSE
	rm -v debian/uftpd/usr/share/doc/uftpd/ChangeLog.md


================================================
FILE: debian/source/format
================================================
3.0 (native)


================================================
FILE: debian/templates
================================================
Template: uftpd/ftp
Type: boolean
Default: true
Description: Enable FTP service?

Template: uftpd/tftp
Type: boolean
Default: true
Description: Enable TFTP service?



================================================
FILE: doc/TODO.md
================================================
TODO
====

* Setup signed .deb repository on deb.troglobit.com
* Port to *BSD (Free/Net/Open) -- requires kqueue support in libuEv
* Add TFTP retransmit support and inactivity timer, see
  http://tools.ietf.org/html/rfc2349
* Add support for IPv6
* Update Coverity Scan model to skip intended constructs
* Add uftp client, with .netrc support
  - See netrc(5) for details of format.
  - Build small CLI library using editline.



================================================
FILE: man/Makefile.am
================================================
dist_man8_MANS     = uftpd.8
SYMLINK            = in.ftpd in.tftpd

# Hook in install to add uftpd.8 --> in.ftpd.8, in.tftpd.8 symlinks
install-data-hook:
	@for file in $(SYMLINK); do \
		link=$(DESTDIR)$(man8dir)/$$file.8; \
		test -e $$link && continue; \
		$(LN_S) $(dist_man8_MANS) $$link; \
	done

uninstall-hook:
	@for file in $(SYMLINK); do \
		$(RM) $(DESTDIR)$(man8dir)/$$file.8; \
	done



================================================
FILE: man/uftpd.8
================================================
.\"
.\" Copyright (c) 2014-2021  Joachim Wiberg <troglobit@gmail.com>
.\"
.\" Permission to use, copy, modify, and/or distribute this software for any
.\" purpose with or without fee is hereby granted, provided that the above
.\" copyright notice and this permission notice appear in all copies.
.\"
.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
.\"
.Dd December 6, 2021
.Dt UFTPD 8
.Os "uftpd (2.14)"
.Sh NAME
.Nm uftpd
.Nd
No nonsense TFTP/FTP Server
.Sh SYNOPSIS
.Nm
.Op Fl hnsv
.Op Fl l Ar LOG
.Op Fl o Ar ftp=PORT,tftp=PORT,writable
.Op Fl p Ar FILE
.Op Ar PATH
.Sh DESCRIPTION
.Nm
is a very simple TFTP and anonymous FTP server with inetd support.  It
listens on standard Internet ports for each protocol, as defined in the
system service specification,
.Pa /etc/services ,
unless other ports are given on the command line.  For details, see
.Xr services 5 .
.Pp
Without any command line arguments
.Nm
serves both FTP and TFTP and automatically backgrounds itself.  Usually
.Nm
this means listen to port 21 (FTP) and port 69 (TFTP), serve files from
.Pa /srv/ftp ,
and log to syslog.  Messages are written to the syslog using the
.Nm LOG_FTP
facility.
.Pp
Available command line options:
.Bl -tag -width Ds
.It Fl h
Show built-in help text
.It Fl l Ar LOG
Set log level: none, err,
.Ar notice ,
info, debug.  By default the log level is
.Ar notice ,
which is less verbose than
.Ar info ,
but still logs all relevant events: users logging in, uploading,
downloading, creating and removing directories, etc.  To reduce
the log level, start
.Nm
with
.Fl l Ar error .
.It Fl n
Run in foreground, do not detach from controlling terminal
.It Fl o
Set
.Nm
option, separate multiple options with comma:
.Bl -tag
.It Ar ftp=PORT
.It Ar tftp=PORT
.It Ar writable
.It Ar pasv_addr=ADDR
.El
.Pp
Override Internet ports otherwise derived from
.Xr services 5 .
Set the
.Ar PORT
to zero (0) to disable a service.
.Pp
The
.Ar writable
option enables writable FTP root, which is not recommended.  Some people
want this, but it is recommended to instead rely on a writable
sub-directory, like
.Ar upload/ ,
or similar.
.Pp
An address passed to the client in passive mode can be overridden with
the
.Ar pasv_addr
option (real data socket address remains unchanged). This may be useful
for passing through some types of NAT.
.It Fl p Ar FILE
File to store process ID for signaling
.Nm .
The default depends on how
.Nm
was configured at build time, and also the UNIX system it runs on,
but often it is found in
.Pa /var/run/uftpd.pid .
.It Fl s
Use syslog, even if running in foreground, default when running in the
background
.It Fl v
Show program version
.It Ar PATH
Root directory. The default is to serve files from the FTP user's $HOME.
When started as root
.Nm
will chroot to this directory as a security measure.
.El
.Pp
.Sh Inetd
.Nm
can also be used with an Internet superserver, like the traditional
inetd or modern init replacements like finit.  In inetd mode the server
takes client connections from stdin.  To enable inetd mode
.Nm
must be called as either
.Nm in.tftpd
or
.Nm in.ftpd .
In inetd mode
.Nm
always runs in the foreground with syslog for messages.
.Pp
.Sh FTP
The file
.Pa /etc/nologin
can be used to disable FTP access.  If the file exists,
.Nm
displays it and exits.  If the file
.Pa /etc/ftpwelcome
exists,
.Nm
prints it before issuing the
.Dq ready
message.
If the file
.Pa /etc/motd
exists,
.Nm
prints it after a successful login.  If the file
.Pa .message
exists in a directory,
.Nm
prints it when that directory is entered.
.Pp
The FTP server currently supports the following requests.
The case of the requests is ignored.
.Bl -column "Request" -offset indent
.It Sy Request Ta Sy "Description"
.It ABOR Ta "abort current transfer"
.It CDUP Ta "shorthand for CD .. command"
.It CWD Ta "change working directory"
.It CLNT Ta "accepted and ignored by server"
.It DELE Ta "delete a file"
.It EPRT Ta "RFC 2428, extended PORT command"
.It EPSV Ta "extended PASV command, used by VLC for Android"
.It FEAT Ta "list supported features"
.It HELP Ta "show help text"
.It LIST Ta "give list files in a directory" Pq Dq Li "ls -lgA"
.It MDTM Ta "RFC 3659, return the last-modified time of a file"
.It MLST Ta "RFC 3659 extension to LIST"
.It MLSD Ta "RFC 3659 extension to LIST"
.It MKD Ta "make a directory"
.It NLST Ta "like LIST, but much less verbose"
.It NOOP Ta "do nothing, used for keep-alive"
.It PASS Ta "specify password"
.It PASV Ta "prepare for server-to-server transfer"
.It PORT Ta "specify data connection port"
.It PWD Ta "print the current working directory"
.It QUIT Ta "terminate session"
.It REST Ta "restore RETR or STOR command at file offset"
.It RETR Ta "retrieve a file"
.It RMD Ta "remove a directory"
.It RNFR Ta "specify rename-from file name"
.It RNTO Ta "specify rename-to file name"
.It SIZE Ta "return size of file"
.It STOR Ta "store a file"
.It SYST Ta "show operating system type of server system"
.It TYPE Ta "specify data transfer" Em type
.It USER Ta "specify user name"
.El
.Pp
Remaining FTP requests, as specified in Internet RFC959, are not
recognized at the moment.  Patches are welcome!
.Pp
.Sh TFTP
.Nm
also supports TFTP, the Trivial File Transfer Protocol, which is
often used for net booting diskless devices, e.g., BOOTP and PXEBOOT.
.Pp
The TFTP server currently supports the following requests.
.Bl -column "Request" -offset indent
.It Sy Request Ta Sy Description
.It RRQ     Ta Read Request for file, may have options
.It WRQ     Ta Write Request for file, may have options
.It DATA    Ta File data, preceded by block n:o
.It ERROR   Ta Error, end of session
.It ACK     Ta ACKnowledge DATA or WRQ without options
.It OACK    Ta Option acknowledged, sent as response to RRQ/WRQ
.El
.Pp
.Nm
supports TFTP blocksize negotiation, according to RFC2348, so full sized
Ethernet frames can be used, which greatly speeds up transfers.
.Pp
.Sh FILES
.Bl -tag -width /etc/ftpwelcome -compact
.It Pa /etc/ftpwelcome
FTP Welcome notice.
.It Pa /etc/motd
Message of the day, presented after successful FTP login.
.It Pa /etc/nologin
Displayed to user attempting to connect.  Access is refused if this
file exists.
.It Pa /var/run/uftpd.pid
Program default PID file, created only when
.Nm
is ready with its internal setup and able to service signals.  Note,
.Nm
exits on most signals.  So no special processing is done atm.
.El
.Sh SEE ALSO
.Xr ftp 1 ,
.Xr tftp 1 ,
.Xr syslogd 8
.Sh AUTHORS
.Nm
was written by Joachim Wiberg
.Aq mailto:troglobit@gmail.com
and is maintained at
.Aq https://github.com/troglobit/uftpd
.Sh BUGS
Here be dragons.


================================================
FILE: src/.gitignore
================================================
uftpd

================================================
FILE: src/Makefile.am
================================================
sbin_PROGRAMS      = uftpd
uftpd_SOURCES      = uftpd.c uftpd.h common.c ftpcmd.c tftpcmd.c log.c
uftpd_CPPFLAGS     = -D_GNU_SOURCE -D_BSD_SOURCE -D_DEFAULT_SOURCE
uftpd_CFLAGS       = -W -Wall -Wextra -Wno-unused-parameter -std=gnu99
uftpd_CFLAGS      += $(uev_CFLAGS) $(lite_CFLAGS)
uftpd_LDADD        = $(uev_LIBS)   $(lite_LIBS)
SYMLINK            = in.ftpd in.tftpd

# Hook in install to add uftpd --> in.ftpd, in.tftpd symlinks
install-exec-hook:
	@for file in $(SYMLINK); do \
		link=$(DESTDIR)$(sbindir)/$$file; \
		test -e $$link && continue; \
		$(LN_S) $(sbin_PROGRAMS) $$link; \
	done

uninstall-hook:
	@for file in $(SYMLINK); do \
		$(RM) $(DESTDIR)$(sbindir)/$$file; \
	done


================================================
FILE: src/common.c
================================================
/* Common methods shared between FTP and TFTP engines
 *
 * Copyright (c) 2014-2021  Joachim Wiberg <troglobit@gmail.com>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include "uftpd.h"

int chrooted = 0;

/* Protect against common directory traversal attacks, for details see
 * https://en.wikipedia.org/wiki/Directory_traversal_attack
 *
 * Example:            /srv/ftp/ ../../etc/passwd => /etc/passwd
 *                    .~~~~~~~~ .~~~~~~~~~
 *                   /         /
 * Server dir ------'         /
 * User input ---------------'
 *
 * Forced dir ------> /srv/ftp/etc
 */
char *compose_path(ctrl_t *ctrl, char *path)
{
	static char rpath[PATH_MAX];
	char dir[PATH_MAX] = { 0 };
	char *name, *ptr;
	struct stat st;

	strlcpy(dir, ctrl->cwd, sizeof(dir));
	DBG("Compose path from cwd: %s, arg: %s", ctrl->cwd, path ?: "");
	if (!path || !strlen(path))
		goto check;

	if (path[0] != '/') {
		if (dir[strlen(dir) - 1] != '/')
			strlcat(dir, "/", sizeof(dir));
	}
	strlcat(dir, path, sizeof(dir));

check:
	while ((ptr = strstr(dir, "//")))
		memmove(ptr, &ptr[1], strlen(&ptr[1]) + 1);

	if (!chrooted) {
		size_t len = strlen(home);

//		DBG("Server path from CWD: %s", dir);
		if (len > 0 && home[len - 1] == '/')
			len--;
		memmove(dir + len, dir, strlen(dir) + 1);
		memcpy(dir, home, len);
//		DBG("Resulting non-chroot path: %s", dir);
	}

	/*
	 * Handle directories slightly differently, since dirname() on a
	 * directory returns the parent directory.  So, just squash ..
	 */
	if (!stat(dir, &st) && S_ISDIR(st.st_mode)) {
		if (!realpath(dir, rpath))
			return NULL;
	} else {
		/*
		 * Check realpath() of directory containing the file, a
		 * STOR may want to save a new file.  Then append the
		 * file and return it.
		 */
		name = basename(path);
		ptr = dirname(dir);

		memset(rpath, 0, sizeof(rpath));
		if (!realpath(ptr, rpath)) {
			INFO("Failed realpath(%s): %m", ptr);
			return NULL;
		}

//		DBG("realpath(%s) => %s", ptr, rpath);

		if (rpath[1] != 0)
			strlcat(rpath, "/", sizeof(rpath));
		strlcat(rpath, name, sizeof(rpath));
	}

	if (!chrooted && strncmp(rpath, home, strlen(home))) {
		DBG("Failed non-chroot dir:%s vs home:%s", dir, home);
		return NULL;
	}

	DBG("Final path to file: %s", rpath);

	return rpath;
}

char *compose_abspath(ctrl_t *ctrl, char *path)
{
	char *ptr;
	char cwd[sizeof(ctrl->cwd)];

	if (path && path[0] == '/') {
		strlcpy(cwd, ctrl->cwd, sizeof(cwd));
		memset(ctrl->cwd, 0, sizeof(ctrl->cwd));
	}

	ptr = compose_path(ctrl, path);

	if (path && path[0] == '/')
		strlcpy(ctrl->cwd, cwd, sizeof(ctrl->cwd));

	return ptr;
}

int set_nonblock(int fd)
{
	int flags;

	flags = fcntl(fd, F_GETFL, 0);
	if (!flags)
		(void)fcntl(fd, F_SETFL, flags | O_NONBLOCK);

	return fd;
}

int open_socket(int port, int type, char *desc)
{
	int sd, err, val = 1;
	socklen_t len = sizeof(struct sockaddr);
	struct sockaddr_in server;

	sd = socket(AF_INET, type | SOCK_NONBLOCK, 0);
	if (sd < 0) {
		WARN(errno, "Failed creating %s server socket", desc);
		return -1;
	}

	err = setsockopt(sd, SOL_SOCKET, SO_REUSEADDR, (char *)&val, sizeof(val));
	if (err != 0)
		WARN(errno, "Failed setting SO_REUSEADDR on %s socket", type == SOCK_DGRAM ? "TFTP" : "FTP");

	memset(&server, 0, sizeof(server));
	server.sin_family      = AF_INET;
	server.sin_addr.s_addr = INADDR_ANY;
	server.sin_port        = htons(port);
	if (bind(sd, (struct sockaddr *)&server, len) < 0) {
		if (EACCES != errno) {
			WARN(errno, "Failed binding to port %d, maybe another %s server is already running", port, desc);
		}
		close(sd);

		return -1;
	}

	if (port && type != SOCK_DGRAM) {
		if (-1 == listen(sd, 20))
			WARN(errno, "Failed starting %s server", desc);
	}

	DBG("Opened socket for port %d", port);

	return sd;
}

void convert_address(struct sockaddr_storage *ss, char *buf, size_t len)
{
	switch (ss->ss_family) {
	case AF_INET:
		inet_ntop(ss->ss_family,
			  &((struct sockaddr_in *)ss)->sin_addr, buf, len);
		break;

	case AF_INET6:
		inet_ntop(ss->ss_family,
			  &((struct sockaddr_in6 *)ss)->sin6_addr, buf, len);
		break;
	}
}

/* Inactivity timer, bye bye */
static void inactivity_cb(uev_t *w, void *arg, int events)
{
	uev_ctx_t *ctx = (uev_ctx_t *)arg;

	INFO("Inactivity timer, exiting ...");
	uev_exit(ctx);
}

ctrl_t *new_session(uev_ctx_t *ctx, int sd, int *rc)
{
	ctrl_t *ctrl = NULL;
	static int privs_dropped = 0;

	if (!inetd) {
		pid_t pid = fork();

		if (pid) {
			DBG("Created new client session as PID %d", pid);
			*rc = pid;
			return NULL;
		}

		/*
		 * Set process group to parent, so uftpd can call
		 * killpg() on all of us when it exits.
		 */
		setpgid(0, getppid());
		/* Create new uEv context for the child. */
		ctx = calloc(1, sizeof(uev_ctx_t));
		if (!ctx) {
			ERR(errno, "Failed allocating session event context");
			exit(1);
		}

		uev_init(ctx);
	}

	ctrl = calloc(1, sizeof(ctrl_t));
	if (!ctrl) {
		ERR(errno, "Failed allocating session context");
		goto fail;
	}

	ctrl->sd = set_nonblock(sd);
	ctrl->ctx = ctx;
	strlcpy(ctrl->cwd, "/", sizeof(ctrl->cwd));

	/* Chroot to FTP root */
	if (!chrooted && geteuid() == 0) {
		if (chroot(home) || chdir("/")) {
			ERR(errno, "Failed chrooting to FTP root, %s, aborting", home);
			goto fail;
		}
		chrooted = 1;
	} else if (!chrooted) {
		if (chdir(home)) {
			WARN(errno, "Failed changing to FTP root, %s, aborting", home);
			goto fail;
		}
	}

	/* If ftp user exists and we're running as root we can drop privs */
	if (!privs_dropped && pw && geteuid() == 0) {
		int fail1, fail2;

		initgroups(pw->pw_name, pw->pw_gid);
		if ((fail1 = setegid(pw->pw_gid)))
			WARN(errno, "Failed dropping group privileges to gid %d", pw->pw_gid);
		if ((fail2 = seteuid(pw->pw_uid)))
			WARN(errno, "Failed dropping user privileges to uid %d", pw->pw_uid);

		setenv("HOME", pw->pw_dir, 1);

		if (!fail1 && !fail2)
			INFO("Successfully dropped privilges to %d:%d (uid:gid)", pw->pw_uid, pw->pw_gid);

		/*
		 * Check we don't have write access to the FTP root,
		 * unless explicitly allowed
		 */
		if (!do_insecure && !access(home, W_OK)) {
			ERR(0, "FTP root %s writable, possible security violation, aborting session!", home);
			goto fail;
		}

		/* On failure, we tried at least.  Only warn once. */
		privs_dropped = 1;
	}

	/* Session timeout handler */
	uev_timer_init(ctrl->ctx, &ctrl->timeout_watcher, inactivity_cb, ctrl->ctx, INACTIVITY_TIMER, 0);

	return ctrl;
fail:
	if (ctrl)
		free(ctrl);
	if (!inetd)
		free(ctx);
	*rc = -1;

	return NULL;
}

int del_session(ctrl_t *ctrl, int isftp)
{
	DBG("%sFTP Client session ended.", isftp ? "": "T" );

	if (!ctrl)
		return -1;

	if (isftp && ctrl->sd > 0) {
		shutdown(ctrl->sd, SHUT_RDWR);
		close(ctrl->sd);
	}

	if (ctrl->data_listen_sd > 0) {
		shutdown(ctrl->data_listen_sd, SHUT_RDWR);
		close(ctrl->data_listen_sd);
	}

	if (ctrl->data_sd > 0) {
		shutdown(ctrl->data_sd, SHUT_RDWR);
		close(ctrl->data_sd);
	}

	if (ctrl->buf)
		free(ctrl->buf);

	if (!inetd && ctrl->ctx)
		free(ctrl->ctx);
	free(ctrl);

	return 0;
}

/**
 * Local Variables:
 *  indent-tabs-mode: t
 *  c-file-style: "linux"
 * End:
 */


================================================
FILE: src/ftpcmd.c
================================================
/* FTP engine
 *
 * Copyright (c) 2014-2021  Joachim Wiberg <troglobit@gmail.com>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include "uftpd.h"
#include <ctype.h>
#include <arpa/ftp.h>
#ifdef HAVE_SYS_TIME_H
# include <sys/time.h>
#endif

#define LISTMODE_LIST 0
#define LISTMODE_NLST 1
#define LISTMODE_MLST 2
#define LISTMODE_MLSD 3

typedef struct {
	char *command;
	void (*cb)(ctrl_t *ctr, char *arg);
} ftp_cmd_t;

static ftp_cmd_t supported[];

static void do_PORT(ctrl_t *ctrl, pend_t pending);
static void do_LIST(uev_t *w, void *arg, int events);
static void do_RETR(uev_t *w, void *arg, int events);
static void do_STOR(uev_t *w, void *arg, int events);

static int is_cont(char *msg)
{
	char *ptr;

	ptr = strchr(msg, '\r');
	if (ptr) {
		ptr++;
		if (strchr(ptr, '\r'))
			return 1;
	}

	return 0;
}

static int send_msg(int sd, char *msg)
{
	int n = 0;
	int l;

	if (!msg) {
	err:
		ERR(EINVAL, "Missing argument to send_msg()");
		return 1;
	}

	l = strlen(msg);
	if (l <= 0)
		goto err;

	while (n < l) {
		int result = send(sd, msg + n, l, 0);

		if (result < 0) {
			ERR(errno, "Failed sending message to client");
			return 1;
		}

		n += result;
	}

	DBG("Sent: %s%s", is_cont(msg) ? "\n" : "", msg);

	return 0;
}

/*
 * Receive message from client, split into command and argument
 */
static int recv_msg(int sd, char *msg, size_t len, char **cmd, char **argument)
{
	char *ptr;
	ssize_t bytes;
	uint8_t *raw = (uint8_t *)msg;

	/* Clear for every new command. */
	memset(msg, 0, len);

	/* Save one byte (-1) for NUL termination */
	bytes = recv(sd, msg, len - 1, 0);
	if (bytes < 0) {
		if (EINTR == errno)
			return 1;

		if (ECONNRESET == errno)
			DBG("Connection reset by client.");
		else
			ERR(errno, "Failed reading client command");
		return 1;
	}

	if (!bytes) {
		INFO("Client disconnected.");
		return 1;
	}

	if (raw[0] == 0xff) {
		char tmp[4];
		char buf[20] = { 0 };
		int i;

		i = recv(sd, &msg[bytes], len - bytes - 1, MSG_OOB | MSG_DONTWAIT);
		if (i > 0)
			bytes += i;

		for (i = 0; i < bytes; i++) {
			snprintf(tmp, sizeof(tmp), "%2X%s", raw[i], i + 1 < bytes ? " " : "");
			strlcat(buf, tmp, sizeof(buf));
		}

		strlcpy(msg, buf, len);
		*cmd      = msg;
		*argument = NULL;

		DBG("Recv: [%s], %zd bytes", msg, bytes);

		return 0;
	}

	/* NUL terminate for strpbrk() */
	msg[bytes] = 0;

	*cmd = msg;
	ptr  = strpbrk(msg, " ");
	if (ptr) {
		*ptr = 0;
		ptr++;
		*argument = ptr;
	} else {
		*argument = NULL;
		ptr = msg;
	}

	ptr = strpbrk(ptr, "\r\n");
	if (ptr)
		*ptr = 0;

	/* Convert command to std ftp upper case, issue #18 */
	for (ptr = msg; *ptr; ++ptr) *ptr = toupper(*ptr);

	DBG("Recv: %s %s", *cmd, *argument ?: "");

	return 0;
}

static int open_data_connection(ctrl_t *ctrl)
{
	socklen_t len = sizeof(struct sockaddr);
	struct sockaddr_in sin = { 0 };

	/* Previous PORT command from client */
	if (ctrl->data_address[0]) {
		int rc;

		ctrl->data_sd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
		if (-1 == ctrl->data_sd) {
			ERR(errno, "Failed creating data socket");
			return -1;
		}

		memset(&sin, 0, sizeof(sin));
		sin.sin_family = AF_INET;
		sin.sin_port = htons(ctrl->data_port);
		inet_aton(ctrl->data_address, &(sin.sin_addr));

		rc = connect(ctrl->data_sd, (struct sockaddr *)&sin, len);
		if (rc == -1 && EINPROGRESS != errno) {
			ERR(errno, "Failed connecting data socket to client");
			close(ctrl->data_sd);
			ctrl->data_sd = -1;

			return -1;
		}

		DBG("Connected successfully to client's previously requested address:PORT %s:%d",
		    ctrl->data_address, ctrl->data_port);
		return 0;
	}

	/* Previous PASV command, accept connect from client */
	if (ctrl->data_listen_sd > 0) {
		const int const_int_1 = 1;
		char client_ip[100];
		int retries = 3;

	retry:
		ctrl->data_sd = accept(ctrl->data_listen_sd, (struct sockaddr *)&sin, &len);
		if (-1 == ctrl->data_sd) {
			if (EAGAIN == errno && --retries) {
				sleep(1);
				goto retry;
			}

			ERR(errno, "Failed accepting connection from client");
			return -1;
		}

		setsockopt(ctrl->data_sd, SOL_SOCKET, SO_KEEPALIVE, &const_int_1, sizeof(const_int_1));
		set_nonblock(ctrl->data_sd);

		inet_ntop(AF_INET, &(sin.sin_addr), client_ip, INET_ADDRSTRLEN);
		DBG("Client PASV data connection from %s:%d", client_ip, ntohs(sin.sin_port));

		close(ctrl->data_listen_sd);
		ctrl->data_listen_sd = -1;
	}

	return 0;
}

static int close_data_connection(ctrl_t *ctrl)
{
	int ret = 0;

	DBG("Closing data connection ...");

	/* PASV server listening socket */
	if (ctrl->data_listen_sd > 0) {
		shutdown(ctrl->data_listen_sd, SHUT_RDWR);
		close(ctrl->data_listen_sd);
		ctrl->data_listen_sd = -1;
		ret++;
	}

	/* PASV client socket */
	if (ctrl->data_sd > 0) {
		shutdown(ctrl->data_sd, SHUT_RDWR);
		close(ctrl->data_sd);
		ctrl->data_sd = -1;
		ret++;
	}

	/* PORT */
	if (ctrl->data_address[0]) {
		ctrl->data_address[0] = 0;
		ctrl->data_port = 0;
	}

	return ret;
}

static int check_user_pass(ctrl_t *ctrl)
{
	if (!ctrl->name[0])
		return -1;

	if (!strcmp("anonymous", ctrl->name))
		return 1;

	return 0;
}

static int do_abort(ctrl_t *ctrl)
{
	if (ctrl->d || ctrl->d_num) {
		uev_io_stop(&ctrl->data_watcher);
		if (ctrl->d_num > 0) {
			int i;

			for (i = 0; i < ctrl->d_num; i++)
				free(ctrl->d[i]);
			free(ctrl->d);
		}
		ctrl->d_num = 0;
		ctrl->d = NULL;
		ctrl->i = 0;

		if (ctrl->file)
			free(ctrl->file);
		ctrl->file = NULL;
	}

	if (ctrl->file) {
		uev_io_stop(&ctrl->data_watcher);
		free(ctrl->file);
		ctrl->file = NULL;
	}

	if (ctrl->fp) {
		fclose(ctrl->fp);
		ctrl->fp = NULL;
	}

	ctrl->pending = PENDING_NONE;
	ctrl->offset = 0;

	return close_data_connection(ctrl);
}

static void handle_ABOR(ctrl_t *ctrl, char *arg)
{
	DBG("Aborting any current transfer ...");
	if (do_abort(ctrl))
		send_msg(ctrl->sd, "426 Connection closed; transfer aborted.\r\n");

	send_msg(ctrl->sd, "226 Closing data connection.\r\n");
}

static void handle_USER(ctrl_t *ctrl, char *name)
{
	if (ctrl->name[0]) {
		ctrl->name[0] = 0;
		ctrl->pass[0] = 0;
	}

	if (name) {
		strlcpy(ctrl->name, name, sizeof(ctrl->name));
		if (check_user_pass(ctrl) == 1) {
			INFO("Guest logged in from %s", ctrl->clientaddr);
			send_msg(ctrl->sd, "230 Guest login OK, access restrictions apply.\r\n");
		} else {
			send_msg(ctrl->sd, "331 Login OK, please enter password.\r\n");
		}
	} else {
		send_msg(ctrl->sd, "530 You must input your name.\r\n");
	}
}

static void handle_PASS(ctrl_t *ctrl, char *pass)
{
	if (!ctrl->name[0]) {
		send_msg(ctrl->sd, "503 No username given.\r\n");
		return;
	}

        if (!pass) {
                send_msg(ctrl->sd, "503 No password given.\r\n");
                return;
        }

	strlcpy(ctrl->pass, pass, sizeof(ctrl->pass));
	if (check_user_pass(ctrl) < 0) {
		LOG("User %s from %s, invalid password!", ctrl->name, ctrl->clientaddr);
		send_msg(ctrl->sd, "530 username or password is unacceptable\r\n");
		return;
	}

	INFO("User %s login from %s", ctrl->name, ctrl->clientaddr);
	send_msg(ctrl->sd, "230 Guest login OK, access restrictions apply.\r\n");
}

static void handle_SYST(ctrl_t *ctrl, char *arg)
{
	char system[] = "215 UNIX Type: L8\r\n";

	send_msg(ctrl->sd, system);
}

static void handle_TYPE(ctrl_t *ctrl, char *argument)
{
	char type[24]  = "200 Type set to I.\r\n";
	char unknown[] = "501 Invalid argument to TYPE.\r\n";

	if (!argument)
		argument = "Z";

	switch (argument[0]) {
	case 'A':
		ctrl->type = TYPE_A; /* ASCII */
		break;

	case 'I':
		ctrl->type = TYPE_I; /* IMAGE/BINARY */
		break;

	default:
		send_msg(ctrl->sd, unknown);
		return;
	}

	type[16] = argument[0];
	send_msg(ctrl->sd, type);
}

static void handle_PWD(ctrl_t *ctrl, char *arg)
{
	char buf[sizeof(ctrl->cwd) + 10];

	snprintf(buf, sizeof(buf), "257 \"%s\"\r\n", ctrl->cwd);
	send_msg(ctrl->sd, buf);
}

static void handle_CWD(ctrl_t *ctrl, char *path)
{
	struct stat st;
	char *dir;

	if (!path)
		goto done;

	/*
	 * Some FTP clients, most notably Chrome, use CWD to check if an
	 * entry is a file or directory.
	 */
	dir = compose_abspath(ctrl, path);
	if (!dir || stat(dir, &st) || !S_ISDIR(st.st_mode)) {
		INFO("%s: CWD: invalid path to %s: %m", ctrl->clientaddr, path);
		send_msg(ctrl->sd, "550 No such directory.\r\n");
		return;
	}

	if (!chrooted)
		dir += strlen(home);

	snprintf(ctrl->cwd, sizeof(ctrl->cwd), "%s", dir);
	if (ctrl->cwd[0] == 0)
		snprintf(ctrl->cwd, sizeof(ctrl->cwd), "/");

done:
	DBG("New CWD: '%s'", ctrl->cwd);
	send_msg(ctrl->sd, "250 OK\r\n");
}

static void handle_CDUP(ctrl_t *ctrl, char *path)
{
	handle_CWD(ctrl, "..");
}

static void handle_PORT(ctrl_t *ctrl, char *str)
{
	int a, b, c, d, e, f;
	char addr[INET_ADDRSTRLEN];
	struct sockaddr_in sin;

	if (ctrl->data_sd > 0) {
		uev_io_stop(&ctrl->data_watcher);
		close(ctrl->data_sd);
		ctrl->data_sd = -1;
	}

        if (!str) {
                send_msg(ctrl->sd, "500 No PORT specified.\r\n");
                return;
        }

	/* Convert PORT command's argument to IP address + port */
	sscanf(str, "%d,%d,%d,%d,%d,%d", &a, &b, &c, &d, &e, &f);
	snprintf(addr, sizeof(addr), "%d.%d.%d.%d", a, b, c, d);

	/* Check IPv4 address using inet_aton(), throw away converted result */
	if (!inet_aton(addr, &(sin.sin_addr))) {
		ERR(0, "Invalid address '%s' given to PORT command", addr);
		send_msg(ctrl->sd, "500 Illegal PORT command.\r\n");
		return;
	}

	strlcpy(ctrl->data_address, addr, sizeof(ctrl->data_address));
	ctrl->data_port = e * 256 + f;

	DBG("Client PORT command accepted for %s:%d", ctrl->data_address, ctrl->data_port);
	send_msg(ctrl->sd, "200 PORT command successful.\r\n");
}

static void handle_EPRT(ctrl_t *ctrl, char *str)
{
	send_msg(ctrl->sd, "502 Command not implemented.\r\n");
}

static char *mode_to_str(mode_t m)
{
	static char str[11];

	snprintf(str, sizeof(str), "%c%c%c%c%c%c%c%c%c%c",
		 S_ISDIR(m)    ? 'd' : '-',
		 (m & S_IRUSR) ? 'r' : '-',
		 (m & S_IWUSR) ? 'w' : '-',
		 (m & S_IXUSR) ? 'x' : '-',
		 (m & S_IRGRP) ? 'r' : '-',
		 (m & S_IWGRP) ? 'w' : '-',
		 (m & S_IXGRP) ? 'x' : '-',
		 (m & S_IROTH) ? 'r' : '-',
		 (m & S_IWOTH) ? 'w' : '-',
		 (m & S_IXOTH) ? 'x' : '-');

	return str;
}

static char *time_to_str(time_t mtime)
{
	struct tm *t = localtime(&mtime);
	static char str[20];

	setlocale(LC_TIME, "C");
	strftime(str, sizeof(str), "%b %e %H:%M", t);

	return str;
}

static char *mlsd_time(time_t mtime)
{
	struct tm *t = localtime(&mtime);
	static char str[20];

	strftime(str, sizeof(str), "%Y%m%d%H%M%S", t);

	return str;
}

static const char *mlsd_type(char *name, int mode)
{
	if (!strcmp(name, "."))
		return "cdir";
	if (!strcmp(name, ".."))
		return "pdir";

	return S_ISDIR(mode) ? "dir" : "file";
}

void mlsd_fact(char fact, char *buf, size_t len, char *name, char *perms, struct stat *st)
{
	char size[20];

	switch (fact) {
	case 'm':
		strlcat(buf, "modify=", len);
		strlcat(buf, mlsd_time(st->st_mtime), len);
		break;

	case 'p':
		strlcat(buf, "perm=", len);
		strlcat(buf, perms, len);
		break;

	case 't':
		strlcat(buf, "type=", len);
		strlcat(buf, mlsd_type(name, st->st_mode), len);
		break;


	case 's':
		if (S_ISDIR(st->st_mode))
			return;
		snprintf(size, sizeof(size), "size=%" PRIu64, st->st_size);
		strlcat(buf, size, len);
		break;

	default:
		return;
	}

	strlcat(buf, ";", len);
}

static void mlsd_printf(ctrl_t *ctrl, char *buf, size_t len, char *path, char *name, struct stat *st)
{
	char perms[10] = "";
	int ro = !access(path, R_OK);
	int rw = !access(path, W_OK);

	if (S_ISDIR(st->st_mode)) {
		/* XXX: Verify 'e' by checking that we can CD to the 'name' */
		if (ro)
			strlcat(perms, "le", sizeof(perms));
		if (rw)
			strlcat(perms, "pc", sizeof(perms)); /* 'd' RMD, 'm' MKD */
	} else {
		if (ro)
			strlcat(perms, "r", sizeof(perms));
		if (rw)
			strlcat(perms, "w", sizeof(perms)); /* 'f' RNFR, 'd' DELE */
	}

	memset(buf, 0, len);
	if (ctrl->d_num == -1 && ctrl->list_mode == LISTMODE_MLST)
		strlcat(buf, " ", len);

	for (int i = 0; ctrl->facts[i]; i++)
		mlsd_fact(ctrl->facts[i], buf, len, name, perms, st);

	strlcat(buf, " ", len);
	strlcat(buf, name, len);
	strlcat(buf, "\r\n", len);
}

static int list_printf(ctrl_t *ctrl, char *buf, size_t len, char *path, char *name)
{
	struct stat st;

	if (stat(path, &st))
		return -1;

	switch (ctrl->list_mode) {
	case LISTMODE_MLSD:
		/* fallthrough */
	case LISTMODE_MLST:
		mlsd_printf(ctrl, buf, len, path, name, &st);
		break;

	case LISTMODE_NLST:
		snprintf(buf, len, "%s\r\n", name);
		break;

	case LISTMODE_LIST:
		snprintf(buf, len, "%s 1 %5d %5d %12" PRIu64 " %s %s\r\n",
			 mode_to_str(st.st_mode),
			 0, 0, (uint64_t)st.st_size,
			 time_to_str(st.st_mtime), name);
		break;
	}

	return 0;
}

static void do_MLST(ctrl_t *ctrl)
{
	char buf[512] = { 0 };
	char cwd[PATH_MAX];
	int sd = ctrl->sd;
	char *path;
	int len;

	if (ctrl->data_sd != -1)
		sd = ctrl->data_sd;

	len = snprintf(buf, sizeof(buf), "250- Listing %s\r\n", ctrl->file);
	if (len < 0 || len > (int)sizeof(buf))
		goto abort;

	strlcpy(cwd, ctrl->file, sizeof(cwd));
	path = compose_path(ctrl, cwd);
	if (!path)
		goto abort;

	if (list_printf(ctrl, &buf[len], sizeof(buf) -  len, path, basename(ctrl->file))) {
	abort:
		do_abort(ctrl);
		send_msg(ctrl->sd, "550 No such file or directory.\r\n");
		return;
	}

	strlcat(buf, "250 End.\r\n", sizeof(buf));
	send_msg(sd, buf);
	do_abort(ctrl);
}

static void do_MLSD(ctrl_t *ctrl)
{
	char buf[512] = { 0 };
	char cwd[PATH_MAX];
	char *path;

	strlcpy(cwd, ctrl->file, sizeof(cwd));
	path = compose_path(ctrl, cwd);
	if (!path)
		goto abort;

	if (list_printf(ctrl, buf, sizeof(buf), path, basename(path))) {
	abort:
		do_abort(ctrl);
		send_msg(ctrl->sd, "550 No such file or directory.\r\n");
		return;
	}

	send_msg(ctrl->data_sd, buf);
	do_abort(ctrl);
	send_msg(ctrl->sd, "226 Transfer complete.\r\n");
}

static void do_LIST(uev_t *w, void *arg, int events)
{
	ctrl_t *ctrl = (ctrl_t *)arg;
	struct timeval tv;
	ssize_t bytes;
	char buf[BUFFER_SIZE] = { 0 };

	if (UEV_ERROR == events || UEV_HUP == events) {
		uev_io_start(w);
		return;
	}

	/* Reset inactivity timer. */
	uev_timer_set(&ctrl->timeout_watcher, INACTIVITY_TIMER, 0);

	if (ctrl->d_num == -1) {
		if (ctrl->list_mode == LISTMODE_MLST)
			do_MLST(ctrl);
		else
			do_MLSD(ctrl);
		return;
	}

	gettimeofday(&tv, NULL);
	if (tv.tv_sec - ctrl->tv.tv_sec > 3) {
		DBG("Sending LIST entry %d of %d to %s ...", ctrl->i, ctrl->d_num, ctrl->clientaddr);
		ctrl->tv.tv_sec = tv.tv_sec;
	}

	while (ctrl->i < ctrl->d_num) {
		struct dirent *entry;
		char cwd[PATH_MAX];
		char *name, *path;
		size_t len;

		entry = ctrl->d[ctrl->i++];
		name  = entry->d_name;

		DBG("Found directory entry %s", name);
		if (!strcmp(name, ".") || !strcmp(name, ".."))
			continue;

		len = strlen(ctrl->file);
		snprintf(cwd, sizeof(cwd), "%s%s%s", ctrl->file,
			 ctrl->file[len > 0 ? len - 1 : len] == '/' ? "" : "/", name);

		path = compose_path(ctrl, cwd);
		if (!path) {
		fail:
			INFO("%s: LIST: Failed reading status for %s: %m", ctrl->clientaddr, path ? path : name);
			continue;
		}

		if (list_printf(ctrl, buf, sizeof(buf), path, name))
			goto fail;

		DBG("LIST %s", buf);

		bytes = send(ctrl->data_sd, buf, strlen(buf), 0);
		if (-1 == bytes) {
			if (ECONNRESET == errno)
				DBG("Connection reset by client.");
			else
				ERR(errno, "Failed sending file %s to client", ctrl->file);

			do_abort(ctrl);
			send_msg(ctrl->sd, "426 TCP connection was established but then broken!\r\n");
		}

		return;
	}

	do_abort(ctrl);
	send_msg(ctrl->sd, "226 Transfer complete.\r\n");
}

static const char *mode2op(int mode)
{
	switch (mode) {
	case LISTMODE_LIST: return "LIST";
	case LISTMODE_NLST: return "NLST";
	case LISTMODE_MLST: return "MLST";
	case LISTMODE_MLSD: return "MLSD";
	}

	return "LST?";
}

static void list(ctrl_t *ctrl, char *arg, int mode)
{
	char *path;

	if (string_valid(arg)) {
		char *ptr, *quot;

		/* Check if client sends ls arguments ... */
		ptr = arg;
		while (*ptr) {
			if (isspace(*ptr))
				ptr++;

			if (*ptr == '-') {
				while (*ptr && !isspace(*ptr))
					ptr++;
			}

			break;
		}

		/* Strip any "" from "<arg>" */
		while ((quot = strchr(ptr, '"'))) {
			char *ptr2;

			ptr2 = strchr(&quot[1], '"');
			if (!ptr2)
				break;

			memmove(ptr2, &ptr2[1], strlen(ptr2));
			memmove(quot, &quot[1], strlen(quot));
		}
		arg = ptr;
	}

	if (mode >= LISTMODE_MLST)
		path = compose_abspath(ctrl, arg);
	else
		path = compose_path(ctrl, arg);
	if (!path) {
		INFO("%s: %s: invalid path to %s: %m", ctrl->clientaddr, mode2op(mode), arg);
		send_msg(ctrl->sd, "550 No such file or directory.\r\n");
		return;
	}

	ctrl->list_mode = mode;
	ctrl->file = strdup(arg ? arg : "");
	ctrl->i = 0;
	ctrl->d_num = scandir(path, &ctrl->d, NULL, alphasort);
	if (ctrl->d_num == -1) {
		if (access(path, R_OK)) {
			send_msg(ctrl->sd, "550 No such file or directory.\r\n");
			DBG("Failed reading directory '%s': %s", path, strerror(errno));
			return;
		}
	}

	DBG("Reading directory %s ... %d number of entries", path, ctrl->d_num);
	if (ctrl->data_sd > -1) {
		send_msg(ctrl->sd, "125 Data connection already open; transfer starting.\r\n");
		uev_io_init(ctrl->ctx, &ctrl->data_watcher, do_LIST, ctrl, ctrl->data_sd, UEV_WRITE);
		return;
	}

	do_PORT(ctrl, PENDING_LIST);
}

static void handle_LIST(ctrl_t *ctrl, char *arg)
{
	list(ctrl, arg, LISTMODE_LIST);
}

static void handle_NLST(ctrl_t *ctrl, char *arg)
{
	list(ctrl, arg, LISTMODE_NLST);
}

static void handle_MLST(ctrl_t *ctrl, char *arg)
{
	list(ctrl, arg, LISTMODE_MLST);
}

static void handle_MLSD(ctrl_t *ctrl, char *arg)
{
	list(ctrl, arg, LISTMODE_MLSD);
}

static void do_pasv_connection(uev_t *w, void *arg, int events)
{
	ctrl_t *ctrl = (ctrl_t *)arg;
	int rc = 0;

	if (UEV_ERROR == events || UEV_HUP == events) {
		DBG("error on data_listen_sd ...");
		uev_io_start(w);
		return;
	}
	DBG("Event on data_listen_sd ...");
	uev_io_stop(&ctrl->data_watcher);
	if (open_data_connection(ctrl))
		return;

	switch (ctrl->pending) {
	case PENDING_STOR:
		/* fallthrough */
	case PENDING_RETR:
		if (ctrl->offset)
			rc = fseek(ctrl->fp, ctrl->offset, SEEK_SET);
		if (rc) {
			do_abort(ctrl);
			send_msg(ctrl->sd, "551 Failed seeking to that position in file.\r\n");
			return;
		}
		/* fallthrough */
	case PENDING_LIST:
		break;

	case PENDING_NONE:
		DBG("No pending command, waiting ...");
		return;
	}

	switch (ctrl->pending) {
	case PENDING_STOR:
		DBG("Pending STOR, starting ...");
		uev_io_init(ctrl->ctx, &ctrl->data_watcher, do_STOR, ctrl, ctrl->data_sd, UEV_READ);
		break;

	case PENDING_RETR:
		DBG("Pending RETR, starting ...");
		uev_io_init(ctrl->ctx, &ctrl->data_watcher, do_RETR, ctrl, ctrl->data_sd, UEV_WRITE);
		break;

	case PENDING_LIST:
		DBG("Pending LIST, starting ...");
		uev_io_init(ctrl->ctx, &ctrl->data_watcher, do_LIST, ctrl, ctrl->data_sd, UEV_WRITE);
		break;

	case PENDING_NONE:
		/* cannot get here */
		return;
	}

	if (ctrl->pending == PENDING_LIST && ctrl->list_mode == LISTMODE_MLST)
		send_msg(ctrl->sd, "150 Opening ASCII mode data connection for MLSD.\r\n");
	else
		send_msg(ctrl->sd, "150 Data connection accepted; transfer starting.\r\n");
	ctrl->pending = PENDING_NONE;
}

static int do_PASV(ctrl_t *ctrl, char *arg, struct sockaddr *data, socklen_t *len)
{
	struct sockaddr_in server;

	if (ctrl->data_sd > 0) {
		close(ctrl->data_sd);
		ctrl->data_sd = -1;
	}

	if (ctrl->data_listen_sd > 0)
		close(ctrl->data_listen_sd);

	ctrl->data_listen_sd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
	if (ctrl->data_listen_sd < 0) {
		ERR(errno, "Failed opening data server socket");
		send_msg(ctrl->sd, "426 Internal server error.\r\n");
		return 1;
	}

	memset(&server, 0, sizeof(server));
	server.sin_family      = AF_INET;
	server.sin_addr.s_addr = inet_addr(ctrl->serveraddr);
	server.sin_port        = htons(0);
	if (bind(ctrl->data_listen_sd, (struct sockaddr *)&server, sizeof(server)) < 0) {
		ERR(errno, "Failed binding to client socket");
		send_msg(ctrl->sd, "426 Internal server error.\r\n");
		close(ctrl->data_listen_sd);
		ctrl->data_listen_sd = -1;
		return 1;
	}

	INFO("Data server port established.  Waiting for client to connect ...");
	if (listen(ctrl->data_listen_sd, 1) < 0) {
		ERR(errno, "Client data connection failure");
		send_msg(ctrl->sd, "426 Internal server error.\r\n");
		close(ctrl->data_listen_sd);
		ctrl->data_listen_sd = -1;
		return 1;
	}

	memset(data, 0, sizeof(*data));
	if (-1 == getsockname(ctrl->data_listen_sd, data, len)) {
		ERR(errno, "Cannot determine our address, need it if client should connect to us");
		close(ctrl->data_listen_sd);
		ctrl->data_listen_sd = -1;
		return 1;
	}

	uev_io_init(ctrl->ctx, &ctrl->data_watcher, do_pasv_connection, ctrl, ctrl->data_listen_sd, UEV_READ);

	return 0;
}

static void handle_PASV(ctrl_t *ctrl, char *arg)
{
	struct sockaddr_in data;
	socklen_t len = sizeof(data);
	char *msg, *p, buf[200];
	int port;

	if (do_PASV(ctrl, arg, (struct sockaddr *)&data, &len))
		return;

	/* Convert server IP address and port to comma separated list */
	if (pasv_addr)
		msg = strdup(pasv_addr);
	else
		msg = strdup(ctrl->serveraddr);
	if (!msg) {
		send_msg(ctrl->sd, "426 Internal server error.\r\n");
		exit(1);
	}
	p = msg;
	while ((p = strchr(p, '.')))
		*p++ = ',';

	port = ntohs(data.sin_port);
	snprintf(buf, sizeof(buf), "227 Entering Passive Mode (%s,%d,%d)\r\n",
		 msg, port / 256, port % 256);
	send_msg(ctrl->sd, buf);

	free(msg);
}

static void handle_EPSV(ctrl_t *ctrl, char *arg)
{
	struct sockaddr_in data;
	socklen_t len = sizeof(data);
	char buf[200];

	if (string_valid(arg) && string_case_compare(arg, "ALL")) {
		send_msg(ctrl->sd, "200 Command OK\r\n");
		return;
	}

	if (do_PASV(ctrl, arg, (struct sockaddr *)&data, &len))
		return;

	snprintf(buf, sizeof(buf), "229 Entering Extended Passive Mode (|||%d|)\r\n", ntohs(data.sin_port));
	send_msg(ctrl->sd, buf);
}

static void do_RETR(uev_t *w, void *arg, int events)
{
	ctrl_t *ctrl = (ctrl_t *)arg;
	struct timeval tv;
	ssize_t bytes;
	size_t num;
	char buf[BUFFER_SIZE];

	if (UEV_ERROR == events || UEV_HUP == events) {
		DBG("error on data_sd ...");
		uev_io_start(w);
		return;
	}

	if (!ctrl->fp) {
		DBG("no fp for RETR, bailing.");
		return;
	}

	num = fread(buf, sizeof(char), sizeof(buf), ctrl->fp);
	if (!num) {
		if (feof(ctrl->fp))
			LOG("User %s from %s downloaded '%s'", ctrl->name, ctrl->clientaddr, ctrl->file);
		else if (ferror(ctrl->fp))
			ERR(0, "Error while reading %s", ctrl->file);
		do_abort(ctrl);
		send_msg(ctrl->sd, "226 Transfer complete.\r\n");
		return;
	}

	/* Reset inactivity timer. */
	uev_timer_set(&ctrl->timeout_watcher, INACTIVITY_TIMER, 0);

	gettimeofday(&tv, NULL);
	if (tv.tv_sec - ctrl->tv.tv_sec > 3) {
		DBG("Sending %zd bytes of %s to %s ...", num, ctrl->file, ctrl->clientaddr);
		ctrl->tv.tv_sec = tv.tv_sec;
	}

	bytes = send(ctrl->data_sd, buf, num, 0);
	if (-1 == bytes) {
		if (ECONNRESET == errno)
			DBG("Connection reset by client.");
		else
			ERR(errno, "Failed sending file %s to client", ctrl->file);

		do_abort(ctrl);
		send_msg(ctrl->sd, "426 TCP connection was established but then broken!\r\n");
	}
}

/*
 * Check if previous command was PORT, then connect to client and
 * transfer file/listing similar to what's done for PASV conns.
 */
static void do_PORT(ctrl_t *ctrl, pend_t pending)
{
	if (!ctrl->data_address[0]) {
		/* Check if previous command was PASV */
		if (ctrl->data_sd == -1 && ctrl->data_listen_sd == -1) {
			if (pending == 1)
				do_MLST(ctrl);
			return;
		}

		ctrl->pending = pending;
		return;
	}

	if (open_data_connection(ctrl)) {
		do_abort(ctrl);
		send_msg(ctrl->sd, "425 TCP connection cannot be established.\r\n");
		return;
	}

	if (pending != PENDING_LIST || ctrl->list_mode != LISTMODE_MLST)
		send_msg(ctrl->sd, "150 Data connection opened; transfer starting.\r\n");

	switch (pending) {
	case PENDING_STOR:
		uev_io_init(ctrl->ctx, &ctrl->data_watcher, do_STOR, ctrl, ctrl->data_sd, UEV_READ);
		break;

	case PENDING_RETR:
		uev_io_init(ctrl->ctx, &ctrl->data_watcher, do_RETR, ctrl, ctrl->data_sd, UEV_WRITE);
		break;

	case PENDING_LIST:
		uev_io_init(ctrl->ctx, &ctrl->data_watcher, do_LIST, ctrl, ctrl->data_sd, UEV_WRITE);
		break;

	default:
		ERR(0, "Unhandled pending command (%d) in %s()!", pending, __func__);
		break;
	}

	ctrl->pending = PENDING_NONE;
}

static void handle_RETR(ctrl_t *ctrl, char *file)
{
	FILE *fp;
	char *path;
	struct stat st;

	path = compose_abspath(ctrl, file);
	if (!path || stat(path, &st)) {
		INFO("%s: RETR: invalid path to %s: %m", ctrl->clientaddr, file);
		send_msg(ctrl->sd, "550 No such file or directory.\r\n");
		return;
	}
	if (!S_ISREG(st.st_mode)) {
		LOG("%s: Failed opening '%s'. Not a regular file", ctrl->clientaddr, path);
		send_msg(ctrl->sd, "550 Not a regular file.\r\n");
		return;
	}

	fp = fopen(path, "rb");
	if (!fp) {
		if (errno != ENOENT)
			ERR(errno, "Failed RETR %s for %s", path, ctrl->clientaddr);
		send_msg(ctrl->sd, "451 Trouble to RETR file.\r\n");
		return;
	}

	ctrl->fp = fp;
	ctrl->file = strdup(file);

	if (ctrl->data_sd > -1) {
		if (ctrl->offset) {
			DBG("Previous REST %ld of file size %ld", ctrl->offset, st.st_size);
			if (fseek(fp, ctrl->offset, SEEK_SET)) {
				do_abort(ctrl);
				send_msg(ctrl->sd, "551 Failed seeking to that position in file.\r\n");
				return;
			}
		}

		send_msg(ctrl->sd, "125 Data connection already open; transfer starting.\r\n");
		uev_io_init(ctrl->ctx, &ctrl->data_watcher, do_RETR, ctrl, ctrl->data_sd, UEV_WRITE);
		return;
	}

	do_PORT(ctrl, PENDING_RETR);
}

/* Request to set mtime, ncftp does this */
static void handle_MDTM(ctrl_t *ctrl, char *file)
{
	struct stat st;
	struct tm *tm;
	char *path, *ptr;
	char *mtime = NULL;
	char buf[80];

        if (!file)
		goto missing;

	ptr = strchr(file, ' ');
	if (ptr) {
		*ptr++ = 0;
		mtime = file;
		file  = ptr;
        }

	path = compose_abspath(ctrl, file);
	if (!path || stat(path, &st) || !S_ISREG(st.st_mode)) {
	missing:
		INFO("MDTM: invalid path to %s: %m", file);
		send_msg(ctrl->sd, "550 Not a regular file.\r\n");
		return;
	}

	if (mtime) {
		struct timespec times[2] = {
			{ 0, UTIME_OMIT },
			{ 0, 0 }
		};
		struct tm tm;
		int rc;

		if (!strptime(mtime, "%Y%m%d%H%M%S", &tm)) {
		fail:
			send_msg(ctrl->sd, "550 Invalid time format\r\n");
			return;
		}

		times[1].tv_sec = mktime(&tm);
		rc = utimensat(0, path, times, 0);
		if (rc) {
			ERR(errno, "Failed setting MTIME %s of %s", mtime, file);
			goto fail;
		}

		LOG("User %s from %s changed mtime of %s", ctrl->name, ctrl->clientaddr, file);
		(void)stat(path, &st);
	}

	tm = gmtime(&st.st_mtime);
	strftime(buf, sizeof(buf), "213 %Y%m%d%H%M%S\r\n", tm);

	send_msg(ctrl->sd, buf);
}

static void do_STOR(uev_t *w, void *arg, int events)
{
	ctrl_t *ctrl = (ctrl_t *)arg;
	struct timeval tv;
	ssize_t bytes;
	size_t num;
	char buf[BUFFER_SIZE];

	if (UEV_ERROR == events || UEV_HUP == events) {
		DBG("error on data_sd ...");
		uev_io_start(w);
		return;
	}

	if (!ctrl->fp) {
		DBG("no fp for STOR, bailing.");
		return;
	}

	/* Reset inactivity timer. */
	uev_timer_set(&ctrl->timeout_watcher, INACTIVITY_TIMER, 0);

	bytes = recv(ctrl->data_sd, buf, sizeof(buf), 0);
	if (bytes < 0) {
		if (ECONNRESET == errno)
			DBG("Connection reset by client.");
		else
			ERR(errno, "Failed receiving file %s from client", ctrl->file);
		do_abort(ctrl);
		send_msg(ctrl->sd, "426 TCP connection was established but then broken!\r\n");
		return;
	}
	if (bytes == 0) {
		LOG("User %s from %s uploaded file %s", ctrl->name, ctrl->clientaddr, ctrl->file);
		do_abort(ctrl);
		send_msg(ctrl->sd, "226 Transfer complete.\r\n");
		return;
	}

	gettimeofday(&tv, NULL);
	if (tv.tv_sec - ctrl->tv.tv_sec > 3) {
		DBG("Receiving %zd bytes of %s from %s ...", bytes, ctrl->file, ctrl->clientaddr);
		ctrl->tv.tv_sec = tv.tv_sec;
	}

	num = fwrite(buf, 1, bytes, ctrl->fp);
	if ((size_t)bytes != num)
		ERR(errno, "552 Disk full.");
}

static void handle_STOR(ctrl_t *ctrl, char *file)
{
	FILE *fp = NULL;
	char *path;
	int rc = 0;

	path = compose_abspath(ctrl, file);
	if (!path) {
		INFO("STOR: invalid path to %s: %m", file);
		goto fail;
	}

	DBG("Trying to write to %s ...", path);
	fp = fopen(path, "wb");
	if (!fp) {
		/* If EACCESS client is trying to do something disallowed */
		ERR(errno, "Failed writing %s", path);
	fail:
		send_msg(ctrl->sd, "451 Trouble storing file.\r\n");
		do_abort(ctrl);
		return;
	}

	ctrl->fp = fp;
	ctrl->file = strdup(file);

	if (ctrl->data_sd > -1) {
		if (ctrl->offset)
			rc = fseek(fp, ctrl->offset, SEEK_SET);
		if (rc) {
			do_abort(ctrl);
			send_msg(ctrl->sd, "551 Failed seeking to that position in file.\r\n");
			return;
		}

		send_msg(ctrl->sd, "125 Data connection already open; transfer starting.\r\n");
		uev_io_init(ctrl->ctx, &ctrl->data_watcher, do_STOR, ctrl, ctrl->data_sd, UEV_READ);
		return;
	}

	do_PORT(ctrl, PENDING_STOR);
}

static void handle_DELE(ctrl_t *ctrl, char *file)
{
	char *path;

	path = compose_abspath(ctrl, file);
	if (!path) {
		INFO("DELE: invalid path to %s: %m", file);
		goto fail;
	}

	if (remove(path)) {
		if (ENOENT == errno)
		fail:	send_msg(ctrl->sd, "550 No such file or directory.\r\n");
		else if (EPERM == errno)
			send_msg(ctrl->sd, "550 Not allowed to remove file or directory.\r\n");
		else if (ENOTEMPTY == errno)
			send_msg(ctrl->sd, "550 Not allowed to remove directory, not empty.\r\n");
		else
			send_msg(ctrl->sd, "550 Unknown error.\r\n");
		return;
	}

	LOG("User %s from %s deleted %s", ctrl->name, ctrl->clientaddr, file);
	send_msg(ctrl->sd, "200 Command OK\r\n");
}

static void handle_MKD(ctrl_t *ctrl, char *arg)
{
	char *path;

	path = compose_abspath(ctrl, arg);
	if (!path) {
		INFO("MKD: invalid path to %s: %m", arg);
		goto fail;
	}

	if (mkdir(path, 0755)) {
		if (EPERM == errno)
		fail:	send_msg(ctrl->sd, "550 Not allowed to create directory.\r\n");
		else
			send_msg(ctrl->sd, "550 Unknown error.\r\n");
		return;
	}

	LOG("User %s from %s created directory %s", ctrl->name, ctrl->clientaddr, arg);
	send_msg(ctrl->sd, "200 Command OK\r\n");
}

static void handle_RMD(ctrl_t *ctrl, char *arg)
{
	handle_DELE(ctrl, arg);
}

static void handle_REST(ctrl_t *ctrl, char *arg)
{
	const char *errstr;
	char buf[80];

	if (!string_valid(arg)) {
		send_msg(ctrl->sd, "550 Invalid argument.\r\n");
		return;
	}

	ctrl->offset = strtonum(arg, 0, INT64_MAX, &errstr);
	snprintf(buf, sizeof(buf), "350 Restarting at %ld.  Send STOR or RETR to continue transfer.\r\n", ctrl->offset);
	send_msg(ctrl->sd, buf);
}

static size_t num_nl(char *file)
{
	FILE *fp;
	char buf[80];
	size_t len, num = 0;

	fp = fopen(file, "r");
	if (!fp)
		return 0;

	do {
		char *ptr = buf;

		len = fread(buf, sizeof(char), sizeof(buf) - 1, fp);
		if (len > 0) {
			buf[len] = 0;
			while ((ptr = strchr(ptr, '\n'))) {
				ptr++;
				num++;
			}
		}
	} while (len > 0);
	fclose(fp);

	return num;
}

static void handle_SIZE(ctrl_t *ctrl, char *file)
{
	char *path;
	char buf[80];
	size_t extralen = 0;
	struct stat st;

	path = compose_abspath(ctrl, file);
	if (!path || stat(path, &st) || S_ISDIR(st.st_mode)) {
		send_msg(ctrl->sd, "550 No such file, or argument is a directory.\r\n");
		return;
	}

	DBG("SIZE %s", path);

	if (ctrl->type == TYPE_A)
		extralen = num_nl(path);

	snprintf(buf, sizeof(buf), "213 %"  PRIu64 "\r\n", (uint64_t)(st.st_size + extralen));
	send_msg(ctrl->sd, buf);
}

/* No operation - used as session keepalive by clients. */
static void handle_NOOP(ctrl_t *ctrl, char *arg)
{
	send_msg(ctrl->sd, "200 NOOP OK.\r\n");
}

#if 0
static void handle_RNFR(ctrl_t *ctrl, char *arg)
{
}

static void handle_RNTO(ctrl_t *ctrl, char *arg)
{
}
#endif

static void handle_QUIT(ctrl_t *ctrl, char *arg)
{
	send_msg(ctrl->sd, "221 Goodbye.\r\n");
	uev_exit(ctrl->ctx);
}

static void handle_CLNT(ctrl_t *ctrl, char *arg)
{
	send_msg(ctrl->sd, "200 CLNT\r\n");
}

static void handle_OPTS(ctrl_t *ctrl, char *arg)
{
	/* OPTS MLST type;size;modify;perm; */
	if (arg && strstr(arg, "MLST")) {
		size_t i = 0;
		char *ptr;
		char buf[42] = "200 MLST OPTS ";
		char facts[10] = { 0 };

		ptr = strtok(arg + 4, " \t;");
		while (ptr && i < sizeof(facts) - 1) {
			if (!strcmp(ptr, "modify") ||
			    !strcmp(ptr, "perm")   ||
			    !strcmp(ptr, "size")   ||
			    !strcmp(ptr, "type")) {
				facts[i++] = ptr[0];
				strlcat(buf, ptr, sizeof(buf));
				strlcat(buf, ";", sizeof(buf));
			}

			ptr = strtok(NULL, ";");
		}
		strlcat(buf, "\r\n", sizeof(buf));

		DBG("New MLSD facts: %s", facts);
		strlcpy(ctrl->facts, facts, sizeof(ctrl->facts));
		send_msg(ctrl->sd, buf);
	} else
		send_msg(ctrl->sd, "200 UTF8 OPTS ON\r\n");
}

static void handle_HELP(ctrl_t *ctrl, char *arg)
{
	ftp_cmd_t *cmd;
	char buf[80];
	int i = 0;

	if (string_valid(arg) && !string_compare(arg, "SITE")) {
		send_msg(ctrl->sd, "500 command HELP does not take any arguments on this server.\r\n");
		return;
	}

	snprintf(ctrl->buf, ctrl->bufsz, "214-The following commands are recognized.");
	for (cmd = &supported[0]; cmd->command; cmd++, i++) {
		if (i % 14 == 0)
			strlcat(ctrl->buf, "\r\n", ctrl->bufsz);
		snprintf(buf, sizeof(buf), " %s", cmd->command);
		strlcat(ctrl->buf, buf, ctrl->bufsz);
	}
	snprintf(buf, sizeof(buf), "\r\n214 Help OK.\r\n");
	strlcat(ctrl->buf, buf, ctrl->bufsz);

	send_msg(ctrl->sd, ctrl->buf);
}

static void handle_FEAT(ctrl_t *ctrl, char *arg)
{
	snprintf(ctrl->buf, ctrl->bufsz, "211-Features:\r\n"
		 " EPSV\r\n"
		 " PASV\r\n"
		 " SIZE\r\n"
		 " UTF8\r\n"
		 " REST STREAM\r\n"
		 " MLST modify*;perm*;size*;type*;\r\n"
		 "211 End\r\n");
	send_msg(ctrl->sd, ctrl->buf);
}

static void handle_UNKNOWN(ctrl_t *ctrl, char *command)
{
	char buf[128];

	snprintf(buf, sizeof(buf), "500 command '%s' not recognized by server.\r\n", command);
	send_msg(ctrl->sd, buf);
}

#define COMMAND(NAME) { #NAME, handle_ ## NAME }

static ftp_cmd_t supported[] = {
	COMMAND(ABOR),
	COMMAND(DELE),
	COMMAND(USER),
	COMMAND(PASS),
	COMMAND(SYST),
	COMMAND(TYPE),
	COMMAND(PORT),
	COMMAND(EPRT),
	COMMAND(RETR),
	COMMAND(MKD),
	COMMAND(RMD),
	COMMAND(REST),
	COMMAND(MDTM),
	COMMAND(PASV),
	COMMAND(EPSV),
	COMMAND(QUIT),
	COMMAND(LIST),
	COMMAND(NLST),
	COMMAND(MLST),
	COMMAND(MLSD),
	COMMAND(CLNT),
	COMMAND(OPTS),
	COMMAND(PWD),
	COMMAND(STOR),
	COMMAND(CWD),
	COMMAND(CDUP),
	COMMAND(SIZE),
	COMMAND(NOOP),
	COMMAND(HELP),
	COMMAND(FEAT),
	{ NULL, NULL }
};

static void child_exit(uev_t *w, void *arg, int events)
{
	DBG("Child exiting ...");
	uev_exit(w->ctx);
}

static void read_client_command(uev_t *w, void *arg, int events)
{
	char *command, *argument;
	ctrl_t *ctrl = (ctrl_t *)arg;
	ftp_cmd_t *cmd;

	if (UEV_ERROR == events || UEV_HUP == events) {
		uev_io_start(w);
		return;
	}

	/* Reset inactivity timer. */
	uev_timer_set(&ctrl->timeout_watcher, INACTIVITY_TIMER, 0);

	if (recv_msg(w->fd, ctrl->buf, ctrl->bufsz, &command, &argument)) {
		DBG("Short read, exiting.");
		uev_exit(ctrl->ctx);
		return;
	}

	if (!string_valid(command))
		return;

	if (string_match(command, "FF F4")) {
		DBG("Ignoring IAC command, client should send ABOR as well.");
		return;
	}

	for (cmd = &supported[0]; cmd->command; cmd++) {
		if (string_compare(command, cmd->command)) {
			cmd->cb(ctrl, argument);
			return;
		}
	}

	handle_UNKNOWN(ctrl, command);
}

static void ftp_command(ctrl_t *ctrl)
{
	uev_t sigterm_watcher;

	ctrl->bufsz = BUFFER_SIZE * sizeof(char);
	ctrl->buf   = malloc(ctrl->bufsz);
	if (!ctrl->buf) {
                WARN(errno, "FTP session failed allocating buffer");
                exit(1);
	}

	snprintf(ctrl->buf, ctrl->bufsz, "220 %s (%s) ready.\r\n", prognm, VERSION);
	send_msg(ctrl->sd, ctrl->buf);

	uev_signal_init(ctrl->ctx, &sigterm_watcher, child_exit, NULL, SIGTERM);
	uev_io_init(ctrl->ctx, &ctrl->io_watcher, read_client_command, ctrl, ctrl->sd, UEV_READ);
	uev_run(ctrl->ctx, 0);
}

int ftp_session(uev_ctx_t *ctx, int sd)
{
	int pid = 0;
	ctrl_t *ctrl;
	socklen_t len;

	ctrl = new_session(ctx, sd, &pid);
	if (!ctrl) {
		if (pid < 0)
			shutdown(sd, SHUT_RDWR);
		close(sd);

		return pid;
	}

	len = sizeof(ctrl->server_sa);
	if (-1 == getsockname(sd, (struct sockaddr *)&ctrl->server_sa, &len)) {
		ERR(errno, "Cannot determine our address");
		goto fail;
	}
	convert_address(&ctrl->server_sa, ctrl->serveraddr, sizeof(ctrl->serveraddr));

	len = sizeof(ctrl->client_sa);
	if (-1 == getpeername(sd, (struct sockaddr *)&ctrl->client_sa, &len)) {
		ERR(errno, "Cannot determine client address");
		goto fail;
	}
	convert_address(&ctrl->client_sa, ctrl->clientaddr, sizeof(ctrl->clientaddr));

	ctrl->type = TYPE_A;
	ctrl->data_listen_sd = -1;
	ctrl->data_sd = -1;
	ctrl->name[0] = 0;
	ctrl->pass[0] = 0;
	ctrl->data_address[0] = 0;
	strlcpy(ctrl->facts, "mpst", sizeof(ctrl->facts));

	INFO("Client connection from %s", ctrl->clientaddr);
	ftp_command(ctrl);

	DBG("Client exiting, bye");
	exit(del_session(ctrl, 1));
fail:
	free(ctrl);
	shutdown(sd, SHUT_RDWR);
	close(sd);

	return -1;
}

/**
 * Local Variables:
 *  indent-tabs-mode: t
 *  c-file-style: "linux"
 * End:
 */


================================================
FILE: src/log.c
================================================
/* uftpd -- the no nonsense (T)FTP server
 *
 * Copyright (c) 2014-2021  Joachim Wiberg <troglobit@gmail.com>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#define SYSLOG_NAMES
#include "uftpd.h"

int loglevel = LOG_NOTICE;


int loglvl(char *level)
{
	for (int i = 0; prioritynames[i].c_name; i++) {
		if (string_match(prioritynames[i].c_name, level))
			return prioritynames[i].c_val;
	}

	return atoi(level);
}

void logit(int severity, const char *fmt, ...)
{
	FILE *file;
        va_list args;

	if (loglevel == INTERNAL_NOPRI)
		return;

	if (severity > LOG_WARNING)
		file = stdout;
	else
		file = stderr;

        va_start(args, fmt);
	if (do_syslog)
		vsyslog(severity, fmt, args);
	else if (severity <= loglevel) {
		if (loglevel == LOG_DEBUG)
			fprintf(file, "%d> ", getpid());
		vfprintf(file, fmt, args);
		fflush(file);
	}
        va_end(args);
}

/**
 * Local Variables:
 *  indent-tabs-mode: t
 *  c-file-style: "linux"
 * End:
 */


================================================
FILE: src/tftpcmd.c
================================================
/* TFTP Engine
 *
 * Copyright (c) 2014-2021  Joachim Wiberg <troglobit@gmail.com>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include "uftpd.h"
#include <poll.h>
#include <arpa/tftp.h>

/*
 * Theory of operation, from RFC1350:
 *
 * Client gets file, RRQ:
 *
 *      client                     server
 *        |----------- RRQ ---------->|
 *        |<--------- DATA -----------|
 *        |----------- ACK ---------->|
 *        |                           |
 *
 * Client puts file, WRQ:
 *
 *      client                     server
 *        |----------- WRQ ---------->|
 *        |<---------- ACK -----------|
 *        |---------- DATA ---------->|
 *        |<---------- ACK -----------|
 *        |                           |
 *
 */

/* Send @len bytes data in @ctrl->buf */
static int do_send(ctrl_t *ctrl, size_t len)
{
	int     result;
	size_t  hdrsz = ctrl->th->th_msg - ctrl->buf;
	size_t  salen = sizeof(struct sockaddr_in);

	if (ctrl->client_sa.ss_family == AF_INET6)
		salen = sizeof(struct sockaddr_in6);

	if (ctrl->th->th_opcode == OACK)
		hdrsz = ctrl->th->th_stuff - ctrl->buf;

	DBG("SND %c: header size: %zd, data len: %zd ...", ctrl->th->th_code, hdrsz, len);
	result = sendto(ctrl->sd, ctrl->buf, hdrsz + len, 0, (struct sockaddr *)&ctrl->client_sa, salen);
	if (-1 == result)
		return 1;

	return 0;
}

/* If @block is non-zero, resend that block */
static int send_DATA(ctrl_t *ctrl, int block)
{
	size_t  len;

	memset(ctrl->buf, 0, ctrl->bufsz);

	/* Create message */
	ctrl->th->th_opcode = htons(DATA);
	if (block) {
		int pos = (block - 1) * ctrl->segsize;

		ctrl->th->th_block = htons(block);
		if (-1 == fseek(ctrl->fp, pos, SEEK_SET)) {
			ERR(errno, "Failed resending block");
			return 1;
		}
	} else {
		ctrl->th->th_block = htons((ftell(ctrl->fp) / ctrl->segsize) + 1);
	}

	DBG("tftp block %d reading %zd bytes ...", ctrl->th->th_block, ctrl->segsize);
	len = fread(ctrl->th->th_data, sizeof(char), ctrl->segsize, ctrl->fp);

	return do_send(ctrl, len);
}

static int send_ACK(ctrl_t *ctrl, int block)
{
	memset(ctrl->buf, 0, ctrl->bufsz);

	ctrl->th->th_opcode = htons(ACK);
	ctrl->th->th_block  = htons(block);
	DBG("ACK block %d", block);

	return do_send(ctrl, 4);
}

/* Acknowledge options sent by client */
static int send_OACK(ctrl_t *ctrl)
{
	char *ptr;

	memset(ctrl->buf, 0, ctrl->bufsz);

	/* Create message */
	ctrl->th->th_opcode = htons(OACK);

	ptr = &ctrl->th->th_stuff[0];
	if (isset(&ctrl->tftp_options, 1)) {
		ptr += sprintf(ptr, "blksize");
		ptr ++;

		ptr += sprintf(ptr, "%zd", ctrl->segsize);
		ptr ++;
	}

	return do_send(ctrl, ptr - ctrl->buf);
}

static int send_ERROR(ctrl_t *ctrl, int code, char *str)
{
	size_t len;

	if (!str)
		str = strerror(code);
	len = strlen(str);

	memset(ctrl->buf, 0, ctrl->segsize);

	/* Create error message */
	ctrl->th->th_opcode = htons(ERROR);
	ctrl->th->th_code   = htons(code);
	strlcpy(ctrl->th->th_msg, str, len);
	DBG("ERR %d: %s", code, str);

	/* Error is ASCIIZ string, hence +1 */
	return do_send(ctrl, len + 1);
}

static int alloc_buf(ctrl_t *ctrl, size_t segsize)
{
	if (!ctrl) {
		errno = EINVAL;
		return 1;
	}

	ctrl->segsize = segsize;
	ctrl->bufsz   = sizeof(tftp_t) + ctrl->segsize;

	if (ctrl->buf)
		ctrl->buf = realloc(ctrl->buf, ctrl->bufsz);
	else
		ctrl->buf = malloc(ctrl->bufsz);

	if (!ctrl->buf)
		return 1;

	ctrl->th = (tftp_t *)ctrl->buf;

	return 0;
}

/* Parse TFTP payload in WRQ/RRQ for filename and optional blksize+timeout */
static int parse_RWRQ(ctrl_t *ctrl, char *buf, size_t len)
{
	size_t opt_len = strlen(buf) + 1;

	/* First opt is always filename */
	ctrl->file = strdup(buf);
	if (!ctrl->file)
		return send_ERROR(ctrl, EUNDEF, NULL);

	do {
		/* Prepare to read options */
		buf += opt_len;
		len -= opt_len;
		opt_len = strlen(buf) + 1;

		if (!strncasecmp(buf, "blksize", 7)) {
			size_t sz = 0;

			buf += opt_len;
			len -= opt_len;
			opt_len = strlen(buf) + 1;

			sscanf(buf, "%zd", &sz);
			if (sz < MIN_SEGSIZE)
				continue; /* Ignore if too small for us. */

			if (alloc_buf(ctrl, sz)) {
				ERR(errno, "Failed reallocating TFTP buffer memory");
				return send_ERROR(ctrl, EUNDEF, NULL);
			}

			DBG("Negotiated blksize %zd", sz);
			setbit(&ctrl->tftp_options, 1);
		}
	} while (len);

	if (!ctrl->tftp_options)
		return 0;

	return send_OACK(ctrl);
}

static int handle_RRQ(ctrl_t *ctrl)
{
	char *path;

	path = compose_path(ctrl, ctrl->file);
	if (!path) {
		ERR(errno, "%s: Invalid path to file %s", ctrl->clientaddr, ctrl->file);
		return send_ERROR(ctrl, ENOTFOUND, NULL);
	}

	ctrl->fp = fopen(path, "r");
	if (!ctrl->fp) {
		ERR(errno, "%s: Failed opening '%s'", ctrl->clientaddr, path);
		return send_ERROR(ctrl, ENOTFOUND, NULL);
	}

	return !send_DATA(ctrl, 0);
}

static int handle_WRQ(ctrl_t *ctrl)
{
	char *path;

	path = compose_path(ctrl, ctrl->file);
	if (!path) {
		ERR(errno, "%s: Invalid path to file %s", ctrl->clientaddr, ctrl->file);
		return send_ERROR(ctrl, ENOTFOUND, NULL);
	}

	ctrl->offset = 1;	/* First expected block */
	ctrl->fp = fopen(path, "w");
	if (!ctrl->fp) {
		ERR(errno, "%s: Failed opening '%s'", ctrl->clientaddr, path);
		return send_ERROR(ctrl, ENOTFOUND, NULL);
	}

	if (ctrl->tftp_options)
		return 0;

	return send_ACK(ctrl, 0);
}

static int handle_DATA(ctrl_t *ctrl, size_t len)
{
	char errmsg[80];
	int block;

	block = ntohs(ctrl->th->th_block);
	if (block != ctrl->offset) {
		snprintf(errmsg, sizeof(errmsg), "Expected block %ld, "
			 "got DATA for block %d", ctrl->offset, block);
		return !send_ERROR(ctrl, EUNDEF, errmsg);
	}

	DBG("tftp block %d writing %zd bytes ...", ctrl->th->th_block, len);
	if (len != fwrite(ctrl->th->th_data, sizeof(char), len, ctrl->fp)) {
		snprintf(errmsg, sizeof(errmsg), "Failed writing file: %s",
			 strerror(errno));
		return !send_ERROR(ctrl, ENOSPACE, errmsg);
	}

	ctrl->offset++;
	if (send_ACK(ctrl, block) || len < ctrl->segsize)
		return 0;

	return 1;
}

/* TODO: Add support for ACK timeout and resend */
static int handle_ACK(ctrl_t *ctrl, int block)
{
	if (ctrl->fp) {
		if (feof(ctrl->fp)) {
			fclose(ctrl->fp);
			ctrl->fp = NULL;
			return 0;
		}

		DBG("ACK block %d, file still open ... ", block);
		return !send_DATA(ctrl, 0);
	}

	return 0;
}

static void read_client_command(uev_t *w, void *arg, int events)
{
	int              active = 1;
	ctrl_t          *ctrl = (ctrl_t *)arg;
	ssize_t          len;
	uint16_t         port, op, block;
	struct sockaddr *addr = (struct sockaddr *)&ctrl->client_sa;
	socklen_t        addr_len = sizeof(ctrl->client_sa);

	/* Reset inactivity timer. */
	uev_timer_set(&ctrl->timeout_watcher, INACTIVITY_TIMER, 0);

	memset(ctrl->buf, 0, ctrl->bufsz);
	len = recvfrom(ctrl->sd, ctrl->buf, ctrl->bufsz, 0, addr, &addr_len);
	if (-1 == len) {
		if (errno != EINTR)
			ERR(errno, "Failed reading command/status from client");

		uev_exit(w->ctx);
		return;
	}

	convert_address(&ctrl->client_sa, ctrl->clientaddr, sizeof(ctrl->clientaddr));
	port   = ntohs(((struct sockaddr_in *)addr)->sin_port);
	op     = ntohs(ctrl->th->th_opcode);
	block  = ntohs(ctrl->th->th_block);

	switch (op) {
	case RRQ:
		len -= ctrl->th->th_stuff - ctrl->buf;
		if (parse_RWRQ(ctrl, ctrl->th->th_stuff, len)) {
			ERR(errno, "Failed parsing TFTP RRQ");
			active = 0;
			break;
		}
		LOG("tftp RRQ '%s' from %s:%d", ctrl->file, ctrl->clientaddr, port);
		active = handle_RRQ(ctrl);
		free(ctrl->file);
		break;

	case WRQ:
		len -= ctrl->th->th_stuff - ctrl->buf;
		if (parse_RWRQ(ctrl, ctrl->th->th_stuff, len)) {
			ERR(errno, "Failed parsing TFTP WRQ");
			active = 0;
			break;
		}
		LOG("tftp WRQ '%s' from %s:%d", ctrl->file, ctrl->clientaddr, port);
		handle_WRQ(ctrl);
		free(ctrl->file);
		break;

	case DATA:		/* Received data after WRQ */
		INFO("tftp DATA '%s' from %s:%d", ctrl->file, ctrl->clientaddr, port);
		len -= ctrl->th->th_data - ctrl->buf;
		active = handle_DATA(ctrl, len);
		break;

	case ERROR:
		DBG("tftp ERROR: %hd", ntohs(ctrl->th->th_code));
		active = 0;
		break;

	case ACK:		/* Sent for each DATA we send in a RRQ */
		DBG("tftp ACK, block # %hu", block);
		active = handle_ACK(ctrl, block);
		break;

	default:
		DBG("tftp opcode: %hd", op);
		DBG("tftp block#: %hu", block);
		break;
	}

	if (!active)
		uev_exit(w->ctx);
}

static void tftp_command(ctrl_t *ctrl)
{
	/* Default buffer and segment size */
	if (alloc_buf(ctrl, SEGSIZE)) {
		ERR(errno, "Failed allocating TFTP buffer memory");
		return;
	}

	uev_io_init(ctrl->ctx, &ctrl->io_watcher, read_client_command, ctrl, ctrl->sd, UEV_READ);
	uev_run(ctrl->ctx, 0);
}

int tftp_session(uev_ctx_t *ctx, int sd)
{
	int pid = 0;
	ctrl_t *ctrl;

	ctrl = new_session(ctx, sd, &pid);
	if (!ctrl)
		return pid;

	tftp_command(ctrl);

	exit(del_session(ctrl, 0));
}

/**
 * Local Variables:
 *  indent-tabs-mode: t
 *  c-file-style: "linux"
 * End:
 */



================================================
FILE: src/uftpd.c
================================================
/* uftpd -- the no nonsense (T)FTP server
 *
 * Copyright (c) 2014-2021  Joachim Wiberg <troglobit@gmail.com>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include "uftpd.h"

/* Global daemon settings */
char *prognm      = PACKAGE_NAME;
char *pidfn       = NULL;
char *home        = NULL;
int   inetd       = 0;
int   background  = 1;
int   do_syslog   = 1;
int   do_ftp      = FTP_DEFAULT_PORT;
int   do_tftp     = TFTP_DEFAULT_PORT;
char *pasv_addr   = NULL;
int   do_insecure = 0;
pid_t tftp_pid    = 0;
struct passwd *pw = NULL;

/* Event contexts */
static uev_t ftp_watcher;
static uev_t tftp_watcher;
static uev_t sigchld_watcher;
static uev_t sigterm_watcher;
static uev_t sigint_watcher;
static uev_t sighup_watcher;
static uev_t sigquit_watcher;


static int version(void)
{
	printf("%s\n", PACKAGE_VERSION);
	printf("\nBug report address: %s\n", PACKAGE_BUGREPORT);
#ifdef PACKAGE_URL
	printf("Project homepage: %s\n", PACKAGE_URL);
#endif
	return 0;
}

static int usage(int code)
{
	int is_inetd = string_match(prognm, "in.");

	if (is_inetd)
		printf("\nUsage: %s [-hv] [-l LEVEL] [PATH]\n\n", prognm);
	else
		printf("\nUsage: %s [-hnsv] [-l LEVEL] [-o OPTS] [-p FILE] [PATH]\n\n", prognm);

	printf("  -h         Show this help text\n"
	       "  -l LEVEL   Set log level: none, err, notice (default), info, debug\n");
	if (!is_inetd)
		printf("  -n         Run in foreground, do not detach from controlling terminal\n"
		       "  -o OPT     Options:\n"
		       "                      ftp=PORT\n"
		       "                      tftp=PORT\n"
		       "                      pasv_addr=ADDR\n"
		       "                      writable\n"
		       "  -p FILE    File to store process ID for signaling %s\n"
		       "  -s         Use syslog, even if running in foreground, default w/o -n\n",
		       prognm);

	printf("  -v         Show program version\n\n");
	printf("The optional 'PATH' defaults to the $HOME of the /etc/passwd user 'ftp'\n");

	return code;
}

/*
 * SIGCHLD: one of our children has died
 */
static void sigchld_cb(uev_t *w, void *arg, int events)
{
	while (1) {
		pid_t pid;

		pid = waitpid(0, NULL, WNOHANG);
		if (pid <= 0)
			break;

		/* TFTP client disconnected, we can now serve TFTP again! */
		if (pid == tftp_pid) {
			DBG("Previous TFTP session ended, restarting TFTP watcher ...");
			tftp_pid = 0;
			uev_io_start(&tftp_watcher);
		}
	}
}

/*
 * SIGQUIT: request termination
 */
static void sigquit_cb(uev_t *w, void *arg, int events)
{
	INFO("Received signal %d, exiting ...", w->signo);

	/* Forward signal to any children in this process group. */
	if (killpg(getpgrp(), SIGTERM))
		WARN(errno, "Failed signalling children");

	/* Give them time to exit gracefully. */
	while (wait(NULL) != -1)
		;

	if (home)
		free(home);

	/* Leave main loop. */
	uev_exit(w->ctx);
}

static void sig_init(uev_ctx_t *ctx)
{
	uev_signal_init(ctx, &sigchld_watcher, sigchld_cb, NULL, SIGCHLD);
	uev_signal_init(ctx, &sigterm_watcher, sigquit_cb, NULL, SIGTERM);
	uev_signal_init(ctx, &sigint_watcher,  sigquit_cb, NULL, SIGINT);
	uev_signal_init(ctx, &sighup_watcher,  sigquit_cb, NULL, SIGHUP);
	uev_signal_init(ctx, &sigquit_watcher, sigquit_cb, NULL, SIGQUIT);
}

static int find_port(char *service, char *proto, int fallback)
{
	int port = fallback;
	struct servent *sv;

	sv = getservbyname(service, proto);
	if (!sv)
		WARN(errno, "Cannot find service %s/%s, defaulting to %d.", service, proto, port);
	else
		port = ntohs(sv->s_port);

	DBG("Found port %d for service %s, proto %s (fallback port %d)", port, service, proto, fallback);

	return port;
}

static int init(uev_ctx_t *ctx)
{
	/* Figure out FTP/TFTP ports */
	if (do_ftp == 1)
		do_ftp  = find_port(FTP_SERVICE_NAME, FTP_PROTO_NAME, FTP_DEFAULT_PORT);
	if (do_tftp == 1)
		do_tftp = find_port(TFTP_SERVICE_NAME, TFTP_PROTO_NAME, TFTP_DEFAULT_PORT); 

	/* Figure out FTP home directory */
	if (!home) {
		pw = getpwnam(FTP_DEFAULT_USER);
		if (!pw) {
			WARN(errno, "Cannot find user %s, falling back to %s as FTP root.",
			     FTP_DEFAULT_USER, FTP_DEFAULT_HOME);
			home = strdup(FTP_DEFAULT_HOME);
		} else {
			home = strdup(pw->pw_dir);
		}
	}

	if (!home || access(home, F_OK)) {
		ERR(errno, "Cannot access FTP root %s", home ? home : "NIL");
		return 1;
	}

	return uev_init(ctx);
}

static void ftp_cb(uev_t *w, void *arg, int events)
{
        int client;

	if (UEV_ERROR == events || UEV_HUP == events) {
		uev_io_stop(w);
		close(w->fd);
		return;
	}

        client = accept(w->fd, NULL, NULL);
        if (client < 0) {
                WARN(errno, "Failed accepting FTP client connection");
                return;
        }

        ftp_session(arg, client);
}

static void tftp_cb(uev_t *w, void *arg, int events)
{
	uev_io_stop(w);

	if (UEV_ERROR == events || UEV_HUP == events) {
		close(w->fd);
		return;
	}

        tftp_pid = tftp_session(arg, w->fd);
	if (tftp_pid < 0) {
		tftp_pid = 0;
		uev_io_start(w);
	}
}

static int start_service(uev_ctx_t *ctx, uev_t *w, uev_cb_t *cb, int port, int type, char *desc)
{
	int sd;

	if (!port)
		/* Disabled */
		return 1;

	sd = open_socket(port, type, desc);
	if (sd < 0) {
		if (EACCES == errno)
			WARN(0, "Not allowed to start %s service.%s",
			     desc, port < 1024 ? "  Privileged port." : "");
		return 1;
	}

	INFO("Starting %s server on port %d ...", desc, port);
	uev_io_init(ctx, w, cb, ctx, sd, UEV_READ);

	return 0;
}

static int serve_files(uev_ctx_t *ctx)
{
	int ftp, tftp;

	DBG("Starting services ...");
	ftp  = start_service(ctx, &ftp_watcher,   ftp_cb, do_ftp, SOCK_STREAM, "FTP");
	tftp = start_service(ctx, &tftp_watcher, tftp_cb, do_tftp, SOCK_DGRAM, "TFTP");

	/* Check if failed to start any service ... */
	if (ftp && tftp)
		return 1;

	/* Setup signal callbacks */
	sig_init(ctx);

	/* We're now up and running, save pid file. */
	pidfile(pidfn);

	INFO("Serving files from %s ...", home);

	return uev_run(ctx, 0);
}

static char *progname(char *arg0)
{
       char *nm;

       nm = strrchr(arg0, '/');
       if (nm)
	       nm++;
       else
	       nm = arg0;

       return nm;
}

int main(int argc, char **argv)
{
	int c;
	enum {
		FTP_OPT = 0,
		TFTP_OPT,
		SEC_OPT,
		PASV_OPT
	};
	char *subopts;
	char *const token[] = {
		[FTP_OPT]  = "ftp",
		[TFTP_OPT] = "tftp",
		[SEC_OPT]  = "writable",
		[PASV_OPT] = "pasv_addr",
		NULL
	};
	uev_ctx_t ctx;
	struct in_addr in_pasv_addr;

	pidfn = prognm = progname(argv[0]);
	while ((c = getopt(argc, argv, "hl:no:p:sv")) != EOF) {
		switch (c) {
		case 'h':
			return usage(0);

		case 'l':
			loglevel = loglvl(optarg);
			if (-1 == loglevel)
				return usage(1);
			break;

		case 'n':
			background = 0;
			do_syslog--;
			break;

		case 'o':
			subopts = optarg;
			while (*subopts != '\0') {
				char *value;

				switch (getsubopt(&subopts, token, &value)) {
				case FTP_OPT:
					if (!value) {
						fprintf(stderr, "Missing port argument to -o ftp=PORT\n");
						return usage(1);
					}
					do_ftp = atoi(value);
					break;

				case TFTP_OPT:
					if (!value) {
						fprintf(stderr, "Missing port argument to -o tftp=PORT\n");
						return usage(1);
					}
					do_tftp = atoi(value);
					break;
				case PASV_OPT:
					if (!value) {
						fprintf(stderr, "Missing PASV address argument to -o pasv_addr=ADDR");
						return usage(1);
					}
					if (!inet_aton(value,&in_pasv_addr)) {
						fprintf(stderr, "Value specified to pasv_addr is not a valid IPv4 address");
						return usage(1);
					}
					pasv_addr = strdup(value);
					break;
				case SEC_OPT:
					do_insecure = 1;
					break;

				default:
					fprintf(stderr, "Unrecognized option '%s'\n", value);
					return usage(1);
				}
			}
			break;

		case 'p':
			pidfn = optarg;
			break;

		case 's':
			do_syslog++;
			break;

		case 'v':
			return version();

		default:
			return usage(1);
		}
	}

	if (optind < argc) {
		home = realpath(argv[optind], NULL);
		if (!home) {
			ERR(errno, "Invalid FTP root %s", argv[optind]);
			return 1;
		}
	}

	/* Inetd mode enforces foreground and syslog */
	if (string_compare(prognm, "in.tftpd")) {
		inetd      = 1;
		do_ftp     = 0;
		do_tftp    = 1;
		background = 0;
		do_syslog  = 1;
	} else if (string_compare(prognm, "in.ftpd")) {
		inetd      = 1;
		do_ftp     = 1;
		do_tftp    = 0;
		background = 0;
		do_syslog  = 1;
	}

	if (do_syslog) {
		openlog(prognm, LOG_PID | LOG_NDELAY, LOG_FTP);
		setlogmask(LOG_UPTO(loglevel));
	}

	DBG("Initializing ...");
	if (init(&ctx)) {
		ERR(0, "Failed initializing, exiting.");
		return 1;
	}

	if (inetd) {
		int sd;
		pid_t pid;

		INFO("Started from inetd, serving files from %s ...", home);

		/* Ensure socket is non-blocking */
		sd = STDIN_FILENO;
		(void)fcntl(sd, F_SETFL, fcntl(sd, F_GETFL, 0) | O_NONBLOCK);

		if (do_tftp)
			pid = tftp_session(&ctx, sd);
		else
			pid = ftp_session(&ctx, sd);

		if (-1 == pid)
			return 1;
		return 0;
	}

	if (background) {
		DBG("Daemonizing ...");
		if (-1 == daemon(0, 0)) {
			ERR(errno, "Failed daemonizing");
			return 1;
		}
	}

	DBG("Serving files as PID %d ...", getpid());
	return serve_files(&ctx);
}

/**
 * Local Variables:
 *  indent-tabs-mode: t
 *  c-file-style: "linux"
 * End:
 */


================================================
FILE: src/uftpd.h
================================================
/* uftpd -- the no nonsense (T)FTP server
 *
 * Copyright (c) 2014-2021  Joachim Wiberg <troglobit@gmail.com>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#ifndef UFTPD_H_
#define UFTPD_H_

#include "config.h"

#include <arpa/inet.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <grp.h>
#include <libgen.h>
#include <limits.h>
#include <locale.h>
#include <netdb.h>
#include <netinet/in.h>
#include <pwd.h>
#include <sched.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>		/*  PRIu64/PRI64, etc. for stdint.h types */
#include <stdlib.h>
#include <string.h>
#include <sys/param.h>		/* isset(), setbit(), etc. */
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <syslog.h>
#include <time.h>
#include <unistd.h>

#include <uev/uev.h>
#ifdef _LIBITE_LITE
# include <libite/lite.h>
#else
# include <lite/lite.h>
#endif

#define FTP_DEFAULT_PORT  21
#define FTP_SERVICE_NAME  "ftp"
#define FTP_PROTO_NAME    "tcp"

#define TFTP_DEFAULT_PORT 69
#define TFTP_SERVICE_NAME "tftp"
#define TFTP_PROTO_NAME   "udp"

#define FTP_DEFAULT_USER  "ftp"
#define FTP_DEFAULT_HOME  "/srv/ftp"

#define BUFFER_SIZE       BUFSIZ

/* This is a stupid server, it doesn't expect >3 min inactivity */
#define INACTIVITY_TIMER  180 * 1000

/* TFTP Packet Types (New) */
#define OACK              06	/* option acknowledgement */

/* TFTP Minimum segment size, specific to uftpd */
#define MIN_SEGSIZE       32

#define LOGIT(severity, code, fmt, args...)				\
	do {								\
		if (code)						\
			logit(severity, fmt ". Error %d: %s%s",		\
			      ##args, code, strerror(code),		\
			      do_syslog ? "" : "\n");			\
		else							\
			logit(severity, fmt "%s", ##args,		\
			      do_syslog ? "" : "\n");			\
	} while (0)
#define ERR(code, fmt, args...)  LOGIT(LOG_ERR, code, fmt, ##args)
#define WARN(code, fmt, args...) LOGIT(LOG_WARNING, code, fmt, ##args)
#define LOG(fmt, args...)        LOGIT(LOG_NOTICE, 0, fmt, ##args)
#define INFO(fmt, args...)       LOGIT(LOG_INFO, 0, fmt, ##args)
#define DBG(fmt, args...)        LOGIT(LOG_DEBUG, 0, fmt, ##args)

extern char *prognm;
extern char *home;		/* Server root/home directory       */
extern int   inetd;             /* Bool: conflicts with daemonize   */
extern int   background;	/* Bool: conflicts with inetd       */
extern int   chrooted;		/* Bool: are we chrooted?           */
extern int   loglevel;
extern int   do_syslog;         /* Bool: False at daemon start      */
extern int   do_ftp;            /* Port: FTP port, or disabled      */
extern int   do_tftp;           /* Port: TFTP port, or disabled     */
extern char *pasv_addr;	/* Address passed to client in pasv mode */
extern int   do_insecure;	/* Bool: Allow writable root or not */
extern struct passwd *pw;       /* FTP user's passwd entry          */

typedef struct tftphdr tftp_t;

typedef enum {
	PENDING_NONE=0,
	PENDING_LIST,
	PENDING_RETR,
	PENDING_STOR
} pend_t;

typedef struct {
	int sd;
	int type;

	char cwd[PATH_MAX];

	struct sockaddr_storage server_sa;
	struct sockaddr_storage client_sa;

	char serveraddr[INET_ADDRSTRLEN];
	char clientaddr[INET_ADDRSTRLEN];

	/* Event loop context and session watchers */
	uev_t      io_watcher, data_watcher, timeout_watcher;
	uev_ctx_t *ctx;

	/* Session buffer */
	char    *buf;		/* Pointer to segment buffer */
	size_t   bufsz;		/* Size of buf */

	char     facts[10];
	pend_t   pending; 	/* Pending op: LIST, RETR, STOR */
	char     list_mode;	/* Current LIST mode */
	char    *file;	        /* Current file name to fetch */
	off_t    offset;	/* Offset/block in current file, for REST/WRQ */
	FILE    *fp;		/* Current file in operation */
	int      i;		/* i of d_num in 'd' */
	int      d_num;		/* Number of entries in 'd' */
	struct dirent **d;	/* Current directory in LIST op */
	struct timeval tv;	/* Progress indicator */

	/* TFTP */
	tftp_t  *th;		/* Same as buf, only as tftp_t */
	size_t   segsize;	/* SEGSIZE, or per session negotiated */
	int      timeout;	/* INACTIVITY_TIMER, or per session neg. */
	uint32_t tftp_options;	/* %1:blksize */

	/* User credentials */
	char name[20];
	char pass[20];

	/* PASV */
	int data_sd;
	int data_listen_sd;

	/* PORT */
	char data_address[INET_ADDRSTRLEN];
	int  data_port;
} ctrl_t;

ctrl_t *new_session(uev_ctx_t *ctx, int sd, int *rc);
int     del_session(ctrl_t *ctrl, int isftp);

int     ftp_session(uev_ctx_t *ctx, int client);
int     tftp_session(uev_ctx_t *ctx, int client);

char   *compose_path(ctrl_t *ctrl, char *path);
char   *compose_abspath(ctrl_t *ctrl, char *path);
int     set_nonblock(int fd);
int     open_socket(int port, int type, char *desc);
void    convert_address(struct sockaddr_storage *ss, char *buf, size_t len);

int     loglvl(char *level);
void    logit(int severity, const char *fmt, ...);

#endif  /* UFTPD_H_ */

/**
 * Local Variables:
 *  indent-tabs-mode: t
 *  c-file-style: "linux"
 * End:
 */


================================================
FILE: test/.gitignore
================================================
*.trs
*.log


================================================
FILE: test/Makefile.am
================================================
EXTRA_DIST         = lib.sh ftp.sh tftp.sh mlst.sh maxfiles.sh
CLEANFILES         = *~ *.trs *.log

TEST_EXTENSIONS    = .sh
TESTS_ENVIRONMENT  = unshare -mrun

TESTS              = ftp.sh
TESTS             += tftp.sh
TESTS             += mlst.sh
TESTS             += maxfiles.sh


================================================
FILE: test/ftp.sh
================================================
#!/bin/sh
#set -x

if [ x"${srcdir}" = x ]; then
    srcdir=.
fi
. ${srcdir}/lib.sh

get()
{
    ftp -n 127.0.0.1 <<-END
	verbose on
    	user anonymous a@b
	bin
	get $1 
	bye
	END
    sleep 1
}

check_dep ftp
get testfile.txt

ls -la
[ -s testfile.txt ] && OK
FAIL



================================================
FILE: test/lib.sh
================================================
#!/bin/sh

# Test name, used everywhere as /tmp/uftpd/$NM/foo
NM=$(basename "$0" .sh)
DIR=/tmp/uftpd/$NM
CDIR=/tmp/uftpd/${NM}-client

# Print heading for test phases
print()
{
	printf "\e[7m>> %-76s\e[0m\n" "$1"
}

dprint()
{
	printf "\e[2m%-76s\e[0m\n" "$1"
}

SKIP()
{
	print "TEST: SKIP"
	[ $# -gt 0 ] && echo "$*"
	exit 77
}

FAIL()
{
	print "TEST: FAIL"
	[ $# -gt 0 ] && echo "$*"
	exit 99
}

OK()
{
	print "TEST: OK"
	[ $# -gt 0 ] && echo "$*"
	exit 0
}

# shellcheck disable=SC2068
check_dep()
{
    if [ -n "$2" ]; then
	if ! $@; then
	    SKIP "$* is not supported on this system."
	fi
    elif ! command -v "$1" >/dev/null; then
	SKIP "Cannot find $1, skipping test."
    fi
}

# Stop all lingering collectors and other tools
kill_pids()
{
	# shellcheck disable=SC2162
	if [ -f "$DIR/PIDs" ]; then
		while read ln; do kill "$ln" 2>/dev/null; done < "$DIR/PIDs"
		rm "$DIR/PIDs"
	fi
}

teardown()
{
	kill_pids
	sleep 1

	[ -d "${DIR}"  ] && rm -rf "${DIR}"
	[ -d "${CDIR}" ] && rm -rf "${CDIR}"
}

signal()
{
	echo
	if [ "$1" != "EXIT" ]; then
		print "Got signal, cleaning up"
	fi
	teardown
}

# props to https://stackoverflow.com/a/2183063/1708249
# shellcheck disable=SC2064
trapit()
{
	func="$1" ; shift
	for sig ; do
		trap "$func $sig" "$sig"
	done
}

setup()
{
	bindir=$(pwd)/../src

	ip link set lo up
	sleep 1

	# https://datatracker.ietf.org/doc/html/rfc3092
	mkdir -p "${DIR}/foo/bar"
	for file in baz qux quz xyzzy; do
		touch "${DIR}/foo/$file"
	done
	for file in fred garply grault waldo; do
		touch "${DIR}/foo/bar/$file"
	done

	cp /etc/passwd "${DIR}/testfile.txt"

	"${bindir}/uftpd" "$DIR" -p "$DIR/pid" >"$DIR/log"
	cd "${CDIR}" || exit 1

	sleep 1
	cat "$DIR/pid" >> "$DIR/PIDs"

	return 0
}

# Runs once when including lib.sh
mkdir -p "${DIR}"
mkdir -p "${CDIR}"
touch "$DIR/PIDs"

# Call signal() on signals or on exit
trapit signal INT TERM QUIT EXIT

# Basic setup for all tests
setup


================================================
FILE: test/maxfiles.sh
================================================
#!/bin/sh
#set -x

if [ x"${srcdir}" = x ]; then
    srcdir=.
fi
. ${srcdir}/lib.sh

#max=`ulimit -n`
max=1040

# check beyond max to verify uftpd doesn't leak descriptors
max=$((max + 20))

get()
{
	ftp -n 127.0.0.1 <<-EOF
		user anonymous a@b
		get testfile.txt
		bye
		EOF
}

check_dep ftp

i=1
while [ $i -lt $max ]; do
    get
    rm testfile.txt
    i=$((i + 1))
done


================================================
FILE: test/mlst.sh
================================================
#!/bin/sh
#set -x

if [ x"${srcdir}" = x ]; then
    srcdir=.
fi
. ${srcdir}/lib.sh

cmd()
{
	tnftp anonymous@127.0.0.1 <<-EOF
		cd foo
		ls
		$*
		EOF
}

check_dep tnftp

cmd mlst bar |grep -q "perm=lepc;type=dir; bar" || FAIL "missing bar"
cmd mlst baz |grep -q "perm=rw;size=0;type=file; baz"  || FAIL "missing baz"

OK


================================================
FILE: test/tftp.sh
================================================
#!/bin/sh
#set -x

if [ x"${srcdir}" = x ]; then
    srcdir=.
fi
. ${srcdir}/lib.sh

get()
{
	tftp 127.0.0.1 -c get "$1"
	sleep 1
}

check_dep tftp
netstat -atnup

get testfile.txt
ls -la
[ -s testfile.txt ] && OK
FAIL

Download .txt
gitextract_aq77ie1o/

├── .github/
│   ├── CODE-OF-CONDUCT.md
│   ├── CONTRIBUTING.md
│   ├── SECURITY.md
│   └── workflows/
│       ├── build.yml
│       ├── coverity.yml
│       └── release.yml
├── .gitignore
├── ChangeLog.md
├── LICENSE
├── Makefile.am
├── README.md
├── autogen.sh
├── configure.ac
├── debian/
│   ├── .gitignore
│   ├── README.Debian
│   ├── changelog
│   ├── compat
│   ├── config
│   ├── control
│   ├── copyright
│   ├── dirs
│   ├── docs
│   ├── postinst
│   ├── postrm
│   ├── prerm
│   ├── rules
│   ├── source/
│   │   └── format
│   └── templates
├── doc/
│   └── TODO.md
├── man/
│   ├── Makefile.am
│   └── uftpd.8
├── src/
│   ├── .gitignore
│   ├── Makefile.am
│   ├── common.c
│   ├── ftpcmd.c
│   ├── log.c
│   ├── tftpcmd.c
│   ├── uftpd.c
│   └── uftpd.h
└── test/
    ├── .gitignore
    ├── Makefile.am
    ├── ftp.sh
    ├── lib.sh
    ├── maxfiles.sh
    ├── mlst.sh
    └── tftp.sh
Download .txt
SYMBOL INDEX (101 symbols across 6 files)

FILE: src/common.c
  type stat (line 38) | struct stat
  function set_nonblock (line 123) | int set_nonblock(int fd)
  function open_socket (line 134) | int open_socket(int port, int type, char *desc)
  function convert_address (line 173) | void convert_address(struct sockaddr_storage *ss, char *buf, size_t len)
  function inactivity_cb (line 189) | static void inactivity_cb(uev_t *w, void *arg, int events)
  function ctrl_t (line 197) | ctrl_t *new_session(uev_ctx_t *ctx, int sd, int *rc)
  function del_session (line 292) | int del_session(ctrl_t *ctrl, int isftp)

FILE: src/ftpcmd.c
  type ftp_cmd_t (line 30) | typedef struct {
  function is_cont (line 42) | static int is_cont(char *msg)
  function send_msg (line 56) | static int send_msg(int sd, char *msg)
  function recv_msg (line 90) | static int recv_msg(int sd, char *msg, size_t len, char **cmd, char **ar...
  function open_data_connection (line 166) | static int open_data_connection(ctrl_t *ctrl)
  function close_data_connection (line 231) | static int close_data_connection(ctrl_t *ctrl)
  function check_user_pass (line 262) | static int check_user_pass(ctrl_t *ctrl)
  function do_abort (line 273) | static int do_abort(ctrl_t *ctrl)
  function handle_ABOR (line 310) | static void handle_ABOR(ctrl_t *ctrl, char *arg)
  function handle_USER (line 319) | static void handle_USER(ctrl_t *ctrl, char *name)
  function handle_PASS (line 339) | static void handle_PASS(ctrl_t *ctrl, char *pass)
  function handle_SYST (line 362) | static void handle_SYST(ctrl_t *ctrl, char *arg)
  function handle_TYPE (line 369) | static void handle_TYPE(ctrl_t *ctrl, char *argument)
  function handle_PWD (line 395) | static void handle_PWD(ctrl_t *ctrl, char *arg)
  function handle_CWD (line 403) | static void handle_CWD(ctrl_t *ctrl, char *path)
  function handle_CDUP (line 434) | static void handle_CDUP(ctrl_t *ctrl, char *path)
  function handle_PORT (line 439) | static void handle_PORT(ctrl_t *ctrl, char *str)
  function handle_EPRT (line 474) | static void handle_EPRT(ctrl_t *ctrl, char *str)
  type tm (line 500) | struct tm
  type tm (line 511) | struct tm
  function mlsd_fact (line 529) | void mlsd_fact(char fact, char *buf, size_t len, char *name, char *perms...
  function mlsd_printf (line 564) | static void mlsd_printf(ctrl_t *ctrl, char *buf, size_t len, char *path,...
  function list_printf (line 595) | static int list_printf(ctrl_t *ctrl, char *buf, size_t len, char *path, ...
  function do_MLST (line 624) | static void do_MLST(ctrl_t *ctrl)
  function do_MLSD (line 656) | static void do_MLSD(ctrl_t *ctrl)
  function do_LIST (line 679) | static void do_LIST(uev_t *w, void *arg, int events)
  function list (line 767) | static void list(ctrl_t *ctrl, char *arg, int mode)
  function handle_LIST (line 834) | static void handle_LIST(ctrl_t *ctrl, char *arg)
  function handle_NLST (line 839) | static void handle_NLST(ctrl_t *ctrl, char *arg)
  function handle_MLST (line 844) | static void handle_MLST(ctrl_t *ctrl, char *arg)
  function handle_MLSD (line 849) | static void handle_MLSD(ctrl_t *ctrl, char *arg)
  function do_pasv_connection (line 854) | static void do_pasv_connection(uev_t *w, void *arg, int events)
  function do_PASV (line 917) | static int do_PASV(ctrl_t *ctrl, char *arg, struct sockaddr *data, sockl...
  function handle_PASV (line 970) | static void handle_PASV(ctrl_t *ctrl, char *arg)
  function handle_EPSV (line 1001) | static void handle_EPSV(ctrl_t *ctrl, char *arg)
  function do_RETR (line 1019) | static void do_RETR(uev_t *w, void *arg, int events)
  function do_PORT (line 1074) | static void do_PORT(ctrl_t *ctrl, pend_t pending)
  function handle_RETR (line 1118) | static void handle_RETR(ctrl_t *ctrl, char *file)
  function handle_MDTM (line 1166) | static void handle_MDTM(ctrl_t *ctrl, char *file)
  function do_STOR (line 1223) | static void do_STOR(uev_t *w, void *arg, int events)
  function handle_STOR (line 1273) | static void handle_STOR(ctrl_t *ctrl, char *file)
  function handle_DELE (line 1316) | static void handle_DELE(ctrl_t *ctrl, char *file)
  function handle_MKD (line 1342) | static void handle_MKD(ctrl_t *ctrl, char *arg)
  function handle_RMD (line 1364) | static void handle_RMD(ctrl_t *ctrl, char *arg)
  function handle_REST (line 1369) | static void handle_REST(ctrl_t *ctrl, char *arg)
  function num_nl (line 1384) | static size_t num_nl(char *file)
  function handle_SIZE (line 1411) | static void handle_SIZE(ctrl_t *ctrl, char *file)
  function handle_NOOP (line 1434) | static void handle_NOOP(ctrl_t *ctrl, char *arg)
  function handle_RNFR (line 1440) | static void handle_RNFR(ctrl_t *ctrl, char *arg)
  function handle_RNTO (line 1444) | static void handle_RNTO(ctrl_t *ctrl, char *arg)
  function handle_QUIT (line 1449) | static void handle_QUIT(ctrl_t *ctrl, char *arg)
  function handle_CLNT (line 1455) | static void handle_CLNT(ctrl_t *ctrl, char *arg)
  function handle_OPTS (line 1460) | static void handle_OPTS(ctrl_t *ctrl, char *arg)
  function handle_HELP (line 1491) | static void handle_HELP(ctrl_t *ctrl, char *arg)
  function handle_FEAT (line 1515) | static void handle_FEAT(ctrl_t *ctrl, char *arg)
  function handle_UNKNOWN (line 1528) | static void handle_UNKNOWN(ctrl_t *ctrl, char *command)
  function child_exit (line 1572) | static void child_exit(uev_t *w, void *arg, int events)
  function read_client_command (line 1578) | static void read_client_command(uev_t *w, void *arg, int events)
  function ftp_command (line 1616) | static void ftp_command(ctrl_t *ctrl)
  function ftp_session (line 1635) | int ftp_session(uev_ctx_t *ctx, int sd)

FILE: src/log.c
  function loglvl (line 24) | int loglvl(char *level)
  function logit (line 34) | void logit(int severity, const char *fmt, ...)

FILE: src/tftpcmd.c
  function do_send (line 45) | static int do_send(ctrl_t *ctrl, size_t len)
  function send_DATA (line 66) | static int send_DATA(ctrl_t *ctrl, int block)
  function send_ACK (line 92) | static int send_ACK(ctrl_t *ctrl, int block)
  function send_OACK (line 104) | static int send_OACK(ctrl_t *ctrl)
  function send_ERROR (line 125) | static int send_ERROR(ctrl_t *ctrl, int code, char *str)
  function alloc_buf (line 145) | static int alloc_buf(ctrl_t *ctrl, size_t segsize)
  function parse_RWRQ (line 169) | static int parse_RWRQ(ctrl_t *ctrl, char *buf, size_t len)
  function handle_RRQ (line 211) | static int handle_RRQ(ctrl_t *ctrl)
  function handle_WRQ (line 230) | static int handle_WRQ(ctrl_t *ctrl)
  function handle_DATA (line 253) | static int handle_DATA(ctrl_t *ctrl, size_t len)
  function handle_ACK (line 280) | static int handle_ACK(ctrl_t *ctrl, int block)
  function read_client_command (line 296) | static void read_client_command(uev_t *w, void *arg, int events)
  function tftp_command (line 374) | static void tftp_command(ctrl_t *ctrl)
  function tftp_session (line 386) | int tftp_session(uev_ctx_t *ctx, int sd)

FILE: src/uftpd.c
  type passwd (line 32) | struct passwd
  function version (line 44) | static int version(void)
  function usage (line 54) | static int usage(int code)
  function sigchld_cb (line 85) | static void sigchld_cb(uev_t *w, void *arg, int events)
  function sigquit_cb (line 106) | static void sigquit_cb(uev_t *w, void *arg, int events)
  function sig_init (line 125) | static void sig_init(uev_ctx_t *ctx)
  function find_port (line 134) | static int find_port(char *service, char *proto, int fallback)
  function init (line 150) | static int init(uev_ctx_t *ctx)
  function ftp_cb (line 178) | static void ftp_cb(uev_t *w, void *arg, int events)
  function tftp_cb (line 197) | static void tftp_cb(uev_t *w, void *arg, int events)
  function start_service (line 213) | static int start_service(uev_ctx_t *ctx, uev_t *w, uev_cb_t *cb, int por...
  function serve_files (line 235) | static int serve_files(uev_ctx_t *ctx)
  function main (line 271) | int main(int argc, char **argv)

FILE: src/uftpd.h
  type passwd (line 106) | struct passwd
  type tftp_t (line 108) | typedef struct tftphdr tftp_t;
  type pend_t (line 110) | typedef enum {
  type ctrl_t (line 117) | typedef struct {
  type sockaddr_storage (line 177) | struct sockaddr_storage
Condensed preview — 46 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (153K chars).
[
  {
    "path": ".github/CODE-OF-CONDUCT.md",
    "chars": 1964,
    "preview": "Contributor Code of Conduct\n===========================\n\nAs contributors and maintainers of this project, and in the int"
  },
  {
    "path": ".github/CONTRIBUTING.md",
    "chars": 2791,
    "preview": "Contributing to uftpd\n=====================\n\nWe welcome any and all help in the form of bug reports, fixes, patches\nfor "
  },
  {
    "path": ".github/SECURITY.md",
    "chars": 348,
    "preview": "# Security Policy\n\n## Supported Versions\n\nuftpd is a small project, as such we have no possibility to support older vers"
  },
  {
    "path": ".github/workflows/build.yml",
    "chars": 1482,
    "preview": "name: Bob the Builder\n\n# Run on all branches, including all pull requests, except the 'dev'\n# branch since that's where "
  },
  {
    "path": ".github/workflows/coverity.yml",
    "chars": 2975,
    "preview": "name: Coverity Scan\n\non:\n  push:\n    branches:\n      - 'dev'\n\nenv:\n  PROJECT_NAME: uftpd\n  CONTACT_EMAIL: troglobit@gmai"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 2074,
    "preview": "name: Release General\n\non:\n  push:\n    tags:\n      - 'v[0-9]+.[0-9]+*'\n\njobs:\n  release:\n    name: Create GitHub release"
  },
  {
    "path": ".gitignore",
    "chars": 234,
    "preview": "*~\n*.o\n*.d\n*.map\n.deps\n.gdb_history\n.libs\n.stamp\n.unpacked\nMakefile\nMakefile.in\naclocal.m4\nar-lib\nautom4te.cache/*\naux/\n"
  },
  {
    "path": "ChangeLog.md",
    "chars": 20608,
    "preview": "Change Log\n==========\n\nAll notable changes to the project are documented in this file.\n\n\n[v2.15][] - 2021-12-20\n--------"
  },
  {
    "path": "LICENSE",
    "chars": 760,
    "preview": "Copyright (C) 2014-2021  Joachim Wiberg <troglobit@gmail.com>\n\nPermission to use, copy, modify, and/or distribute this s"
  },
  {
    "path": "Makefile.am",
    "chars": 1458,
    "preview": "SUBDIRS            = src man test\ndoc_DATA           = README.md LICENSE ChangeLog.md\nEXTRA_DIST         = README.md LIC"
  },
  {
    "path": "README.md",
    "chars": 6235,
    "preview": "No Nonsense FTP/TFTP Server\n===========================\n[![License Badge][]][License] [![GitHub Status][]][GitHub] [![Co"
  },
  {
    "path": "autogen.sh",
    "chars": 44,
    "preview": "#!/bin/sh\n\nautoreconf -W portability -visfm\n"
  },
  {
    "path": "configure.ac",
    "chars": 765,
    "preview": "AC_INIT([uftpd], [2.15], [https://github.com/troglobit/uftpd/issues], [],\n\t[https://troglobit.com/projects/uftpd/])\nAC_C"
  },
  {
    "path": "debian/.gitignore",
    "chars": 107,
    "preview": "autoreconf.*\ndebhelper-build-stamp\nfiles\nuftpd.debhelper.log\nuftpd.post*\nuftpd.pre*\nuftpd.substvars\nuftpd/\n"
  },
  {
    "path": "debian/README.Debian",
    "chars": 321,
    "preview": "uftpd for Debian/Ubuntu\n-----------------------\n\nuftpd is a true UNIX TFTP/FTP daemon, it serves files, and nothing more"
  },
  {
    "path": "debian/changelog",
    "chars": 9565,
    "preview": "uftpd (2.15) stable; urgency=medium\n\n  * Silence some developer debug messages\n  * Always skip `.` and `..` in FTP listi"
  },
  {
    "path": "debian/compat",
    "chars": 3,
    "preview": "10\n"
  },
  {
    "path": "debian/config",
    "chars": 154,
    "preview": "#!/bin/sh\n\nset -e\n. /usr/share/debconf/confmodule\n\ndb_title uftpd\n\ndb_input critical uftpd/ftp || true\ndb_go\n\ndb_input c"
  },
  {
    "path": "debian/control",
    "chars": 989,
    "preview": "Source: uftpd\nSection: net\nPriority: optional\nMaintainer: Joachim Wiberg <troglobit@gmail.com>\nBuild-Depends: debhelper "
  },
  {
    "path": "debian/copyright",
    "chars": 787,
    "preview": "\nCopyright: (c) 2014-2021  Joachim Wiberg <troglobit@gmail.com>\n\nLicense: ISC\n Permission to use, copy, modify, and/or d"
  },
  {
    "path": "debian/dirs",
    "chars": 28,
    "preview": "usr/share/man/man8\nusr/sbin\n"
  },
  {
    "path": "debian/docs",
    "chars": 10,
    "preview": "README.md\n"
  },
  {
    "path": "debian/postinst",
    "chars": 1477,
    "preview": "#!/bin/sh\n\nset -e\n\n[ \"$1\" = \"configure\" ] || exit 0\n\n# Source debconf library.\n. /usr/share/debconf/confmodule\n\nFTPENTRY"
  },
  {
    "path": "debian/postrm",
    "chars": 418,
    "preview": "#!/bin/sh\nset -e\n\nif [ \"$1\" = \"purge\" ]; then\n\tif command -v update-inetd >/dev/null 2>&1; then\n\t\tupdate-inetd --pattern"
  },
  {
    "path": "debian/prerm",
    "chars": 131,
    "preview": "#!/bin/sh\n\nset -e\n\nupdate-inetd --pattern 'in.ftpd' --multi --disable ftp\nupdate-inetd --pattern 'in.tftpd' --multi --di"
  },
  {
    "path": "debian/rules",
    "chars": 428,
    "preview": "#!/usr/bin/make -f\n# export DH_VERBOSE=1\nexport DEB_BUILD_MAINT_OPTIONS = hardening=+all\nexport DEB_BUILD_OPTIONS='paral"
  },
  {
    "path": "debian/source/format",
    "chars": 13,
    "preview": "3.0 (native)\n"
  },
  {
    "path": "debian/templates",
    "chars": 166,
    "preview": "Template: uftpd/ftp\nType: boolean\nDefault: true\nDescription: Enable FTP service?\n\nTemplate: uftpd/tftp\nType: boolean\nDef"
  },
  {
    "path": "doc/TODO.md",
    "chars": 428,
    "preview": "TODO\n====\n\n* Setup signed .deb repository on deb.troglobit.com\n* Port to *BSD (Free/Net/Open) -- requires kqueue support"
  },
  {
    "path": "man/Makefile.am",
    "chars": 398,
    "preview": "dist_man8_MANS     = uftpd.8\nSYMLINK            = in.ftpd in.tftpd\n\n# Hook in install to add uftpd.8 --> in.ftpd.8, in.t"
  },
  {
    "path": "man/uftpd.8",
    "chars": 7058,
    "preview": ".\\\"\n.\\\" Copyright (c) 2014-2021  Joachim Wiberg <troglobit@gmail.com>\n.\\\"\n.\\\" Permission to use, copy, modify, and/or di"
  },
  {
    "path": "src/.gitignore",
    "chars": 5,
    "preview": "uftpd"
  },
  {
    "path": "src/Makefile.am",
    "chars": 691,
    "preview": "sbin_PROGRAMS      = uftpd\nuftpd_SOURCES      = uftpd.c uftpd.h common.c ftpcmd.c tftpcmd.c log.c\nuftpd_CPPFLAGS     = -"
  },
  {
    "path": "src/common.c",
    "chars": 7795,
    "preview": "/* Common methods shared between FTP and TFTP engines\n *\n * Copyright (c) 2014-2021  Joachim Wiberg <troglobit@gmail.com"
  },
  {
    "path": "src/ftpcmd.c",
    "chars": 37709,
    "preview": "/* FTP engine\n *\n * Copyright (c) 2014-2021  Joachim Wiberg <troglobit@gmail.com>\n *\n * Permission to use, copy, modify,"
  },
  {
    "path": "src/log.c",
    "chars": 1640,
    "preview": "/* uftpd -- the no nonsense (T)FTP server\n *\n * Copyright (c) 2014-2021  Joachim Wiberg <troglobit@gmail.com>\n *\n * Perm"
  },
  {
    "path": "src/tftpcmd.c",
    "chars": 9522,
    "preview": "/* TFTP Engine\n *\n * Copyright (c) 2014-2021  Joachim Wiberg <troglobit@gmail.com>\n *\n * Permission to use, copy, modify"
  },
  {
    "path": "src/uftpd.c",
    "chars": 9863,
    "preview": "/* uftpd -- the no nonsense (T)FTP server\n *\n * Copyright (c) 2014-2021  Joachim Wiberg <troglobit@gmail.com>\n *\n * Perm"
  },
  {
    "path": "src/uftpd.h",
    "chars": 5643,
    "preview": "/* uftpd -- the no nonsense (T)FTP server\n *\n * Copyright (c) 2014-2021  Joachim Wiberg <troglobit@gmail.com>\n *\n * Perm"
  },
  {
    "path": "test/.gitignore",
    "chars": 12,
    "preview": "*.trs\n*.log\n"
  },
  {
    "path": "test/Makefile.am",
    "chars": 280,
    "preview": "EXTRA_DIST         = lib.sh ftp.sh tftp.sh mlst.sh maxfiles.sh\nCLEANFILES         = *~ *.trs *.log\n\nTEST_EXTENSIONS    ="
  },
  {
    "path": "test/ftp.sh",
    "chars": 267,
    "preview": "#!/bin/sh\n#set -x\n\nif [ x\"${srcdir}\" = x ]; then\n    srcdir=.\nfi\n. ${srcdir}/lib.sh\n\nget()\n{\n    ftp -n 127.0.0.1 <<-END"
  },
  {
    "path": "test/lib.sh",
    "chars": 1920,
    "preview": "#!/bin/sh\n\n# Test name, used everywhere as /tmp/uftpd/$NM/foo\nNM=$(basename \"$0\" .sh)\nDIR=/tmp/uftpd/$NM\nCDIR=/tmp/uftpd"
  },
  {
    "path": "test/maxfiles.sh",
    "chars": 374,
    "preview": "#!/bin/sh\n#set -x\n\nif [ x\"${srcdir}\" = x ]; then\n    srcdir=.\nfi\n. ${srcdir}/lib.sh\n\n#max=`ulimit -n`\nmax=1040\n\n# check "
  },
  {
    "path": "test/mlst.sh",
    "chars": 323,
    "preview": "#!/bin/sh\n#set -x\n\nif [ x\"${srcdir}\" = x ]; then\n    srcdir=.\nfi\n. ${srcdir}/lib.sh\n\ncmd()\n{\n\ttnftp anonymous@127.0.0.1 "
  },
  {
    "path": "test/tftp.sh",
    "chars": 220,
    "preview": "#!/bin/sh\n#set -x\n\nif [ x\"${srcdir}\" = x ]; then\n    srcdir=.\nfi\n. ${srcdir}/lib.sh\n\nget()\n{\n\ttftp 127.0.0.1 -c get \"$1\""
  }
]

About this extraction

This page contains the full source code of the troglobit/uftpd GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 46 files (137.2 KB), approximately 42.6k tokens, and a symbol index with 101 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!