diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ef77469..030327b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,10 +18,16 @@ jobs: - uses: actions/checkout@v3 - name: Install APT dependencies - run: sudo apt install libfuse3-dev libwecksockets-dev + run: sudo apt install libfuse3-dev libwebsockets-dev libgtest-dev libgmock-dev clang-tidy valgrind - name: Configure CMake run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} - name: Build run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} + + - name: Unit Test + run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --target test + + - name: Memcheck + run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --target memcheck diff --git a/.gitignore b/.gitignore index f3d6549..c07155d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -/build/ \ No newline at end of file +/build/ +/.vscode/ +*.pem diff --git a/CMakeLists.txt b/CMakeLists.txt index 03299b0..75525b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,129 @@ cmake_minimum_required(VERSION 3.10) project(webfuse VERSION 2.0.0) -add_executable(webfuse - src/main.cpp) +option(WITHOUT_PROVIDER "Disabled build of webfuse provider" OFF) +option(WITHOUT_TEST "Disables unit and integration tests" OFF) +option(WITHOUT_CLANG_TIDY "Disables clang tidy" OFF) + +set (CMAKE_CXX_STANDARD 17) + +find_package(PkgConfig REQUIRED) +pkg_check_modules(FUSE REQUIRED IMPORTED_TARGET fuse3) +pkg_check_modules(LWS REQUIRED IMPORTED_TARGET libwebsockets) + +configure_file(src/webfuse/version.cpp.in version.cpp) + +add_library(webfuse_static STATIC + ${CMAKE_CURRENT_BINARY_DIR}/version.cpp + src/webfuse/webfuse.cpp + src/webfuse/provider.cpp + src/webfuse/fuse.cpp + src/webfuse/request_type.cpp + src/webfuse/response_type.cpp + src/webfuse/util/commandline_args.cpp + src/webfuse/util/commandline_reader.cpp + src/webfuse/util/authenticator.cpp + src/webfuse/filesystem.cpp + src/webfuse/filesystem/status.cpp + src/webfuse/filesystem/accessmode.cpp + src/webfuse/filesystem/openflags.cpp + src/webfuse/filesystem/filemode.cpp + src/webfuse/filesystem/filesystem_statistics.cpp + src/webfuse/filesystem/empty_filesystem.cpp + src/webfuse/ws/config.cpp + src/webfuse/ws/server.cpp + src/webfuse/ws/server_handler.cpp + src/webfuse/ws/client.cpp + src/webfuse/ws/messagewriter.cpp + src/webfuse/ws/messagereader.cpp + src/webfuse/ws/url.cpp +) + +target_include_directories(webfuse_static PUBLIC src) +target_link_libraries(webfuse_static PUBLIC PkgConfig::FUSE PkgConfig::LWS) + +if(NOT(WITHOUT_CLANG_TIDY)) +set_property( + TARGET webfuse_static + PROPERTY CXX_CLANG_TIDY clang-tidy -checks=readability-*,-readability-identifier-length -warnings-as-errors=*) +endif() + + +add_executable(webfuse src/main.cpp) +target_link_libraries(webfuse PRIVATE webfuse_static) +install(TARGETS webfuse DESTINATION bin) + + +if(NOT(WITHOUT_PROVIDER)) + +add_executable(webfuse_provider src/provider_main.cpp) +target_link_libraries(webfuse_provider PRIVATE webfuse_static) +install(TARGETS webfuse_provider DESTINATION bin) + +endif() + +if(NOT(WITHOUT_TEST)) + + pkg_check_modules(GTEST REQUIRED gtest_main) + pkg_check_modules(GMOCK REQUIRED gmock) + + add_executable(unit_tests + test-src/unit/webfuse/test_app.cpp + test-src/unit/webfuse/test_request_type.cpp + test-src/unit/webfuse/test_response_type.cpp + test-src/unit/webfuse/filesystem/test_status.cpp + test-src/unit/webfuse/filesystem/test_accessmode.cpp + test-src/unit/webfuse/filesystem/test_openflags.cpp + test-src/unit/webfuse/filesystem/test_filemode.cpp + ) + + target_include_directories(unit_tests PRIVATE test-src/unit ${GTEST_INCLUDE_DIRS} ${GMOCK_INCLUDE_DIRS}) + target_compile_options(unit_tests PRIVATE + ${GTEST_CFLAGS} ${GTEST_CFLAGS_OTHER} + ${GMOCK_CFLAGS} ${GMOCK_CFLAGS_OTHER} + ) + target_link_libraries(unit_tests PRIVATE webfuse_static ${GTEST_LIBRARIES} ${GMOCK_LIBRARIES}) + + add_executable(integration_tests + test-src/integration/webfuse/test/tempdir.cpp + test-src/integration/webfuse/test/fixture.cpp + test-src/integration/webfuse/test/process.cpp + test-src/integration/webfuse/test/daemon.cpp + test-src/integration/test_access.cpp + test-src/integration/test_readdir.cpp + test-src/integration/test_readlink.cpp + test-src/integration/test_symlink.cpp + test-src/integration/test_link.cpp + test-src/integration/test_rename.cpp + test-src/integration/test_chmod.cpp + test-src/integration/test_chown.cpp + test-src/integration/test_truncate.cpp + test-src/integration/test_fsync.cpp + test-src/integration/test_utimens.cpp + test-src/integration/test_open.cpp + test-src/integration/test_mknod.cpp + test-src/integration/test_unlink.cpp + test-src/integration/test_read.cpp + test-src/integration/test_write.cpp + test-src/integration/test_mkdir.cpp + test-src/integration/test_rmdir.cpp + test-src/integration/test_statfs.cpp + ) + + target_include_directories(integration_tests PRIVATE test-src/integration ${GTEST_INCLUDE_DIRS} ${GMOCK_INCLUDE_DIRS}) + target_compile_options(integration_tests PRIVATE + ${GTEST_CFLAGS} ${GTEST_CFLAGS_OTHER} + ${GMOCK_CFLAGS} ${GMOCK_CFLAGS_OTHER} + ) + target_link_libraries(integration_tests PRIVATE webfuse_static ${GTEST_LIBRARIES} ${GMOCK_LIBRARIES}) + + enable_testing() + add_test(NAME unit_tests COMMAND unit_tests) + add_test(NAME integration_tests COMMAND integration_tests) + + find_program(VALGRIND valgrind REQUIRED) + if(VALGRIND) + add_custom_target(memcheck COMMAND valgrind --leak-check=full --error-exitcode=1 ./unit_tests) + endif() + +endif() diff --git a/README.md b/README.md index 19b5895..baeb335 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,75 @@ -# webfuse2 +[![build](https://github.com/falk-werner/webfuse/actions/workflows/build.yml/badge.svg)](https://github.com/falk-werner/webfuse/actions/workflows/build.yml) -Reimplementation of webfuse. +# webfuse -## Build +webfuse combines libwebsockets and libfuse. It allows to attach a remote filesystem via websockets. -```` -cmake -B build -cmake --build build -```` \ No newline at end of file +## Motivation + +Many embedded devices, such as smart home or [IoT](https://en.wikipedia.org/wiki/Internet_of_things) devices are very limited regarding to their (non-volatile) memory resources. Such devices are typically comprised of an embedded linux and a small web server, providing an interface for maintenance purposes. + +Some use cases, such as firmware update, require to transfer (larger) files to the device. The firmware file is often stored multiple times on the device: + +1. cached by the web server, e.g. [lighttpd](https://redmine.lighttpd.net/boards/2/topics/3451) +2. copied to locally, e.g. /tmp +3. uncompressed, also to /tmp + +Techniques like [SquashFS](https://en.wikipedia.org/wiki/SquashFS) help to avoid the third step, since the upgrade file can be mounted directly. [RAUC](https://rauc.io/) shows the use of SquashFS within an update facility. +However at least one (unecessary) copy of the upload file is needed on the device. + +To avoid Steps 1 and 2, it would be great to keep the update file entirely in web server, just like [NFS](https://en.wikipedia.org/wiki/Network_File_System) or [WebDAV](https://wiki.archlinux.org/index.php/WebDAV). Unfortunately, NFS is not based on any protocol, natively usable by a web application. WebDAV is based on HTTP, but it needs a server providing the update file. + +webfuse solves this problem by using the [WebSocket](https://en.wikipedia.org/wiki/WebSocket) protocol. The emdedded device runs a service, known as webfuse adapter, awaiting incoming connections, e.g. from a web browser. The browser acts as a file system provider, providing the update file to the device. + +## Concept + +![concept](doc/concept.png) + +With webfuse it is possible to implement remote filesystems based on websockets. +Therefore, webfuse defined two roles participating in a webfuse connection: + +- webfuse service +- webfuse provider + +### Webfuse Service + +A `webfuse service` is both, +- a [websocket](https://en.wikipedia.org/wiki/WebSocket) service providing the `webfuse` protocol +- a [fuse](https://github.com/libfuse/libfuse) filesystem attached to a local mountpoint + +The `webfuse service` awaits incoming connections from a `webfuse provider`. Once connected, it communicates all the filesystem requests originated by the `libfuse` to the connected `webfuse provider` using the `websocket`-based `webfuse protocol`. + +By doing so, `webfuse` allows to inject a filesystem to a remote device. + +### Webfuse Provider + +A `webfuse provider` provides a filesystem to a remote device using the `websocket`-based `webfuse protocol`. Therefore, a `webfuse provider` implements a `websocket` client. + + +## Similar Projects + +### Davfs2 + +[davfs2](http://savannah.nongnu.org/projects/davfs2) is a Linux file system driver that allows to mount a [WebDAV](https://wiki.archlinux.org/index.php/WebDAV) resource. WebDAV is an extension to HTTP/1.1 that allows remote collaborative authoring of Web resources. + +Unlike webfuse, davfs2 mounts a remote filesystem locally, that is provided by a WebDAV server. In contrast, webfuse starts a server awaiting client connections to attach the remote file system. + +## Further Documentation + +- [Build instructions](doc/build.md) +- [Webfuse command line options](doc/webfuse.md) +- [Webfuse provider command line options](doc/webfuse_provider.md) +- [Webfuse Protocol](doc/protocol.md) +- [Authentication](doc/authentication.md) + +## webfuse legacy + +`webfuse2` is a complete re-implementation of the original idea behind `webfuse`. In contrast to the original `webfuse` implementation, `webfuse2` provides also write access to the filesystem and allows to use all standard options of a fuse filesystem. + +But `webfuse2` marks also some breaking changes: + +- `webfuse2` uses a new, binary protocol which is not compatible to the JSON-based protocol of legacy `webfuse` +- `webfuse` does not provide an API nor a library to program against + _(if you are interested in an API or a library for webfuse2 feel free to create an issue)_ + +When you are interested in the original `webfuse` implementation take a look at this [branch](https://github.com/falk-werner/webfuse/tree/master). diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..4bd4579 --- /dev/null +++ b/doc/README.md @@ -0,0 +1,7 @@ +# webfuse developer documentation + +- [Build instructions](build.md) +- [Webfuse command line options](webfuse.md) +- [Webfuse provider command line options](webfuse_provider.md) +- [Webfuse2 protocol](protocol.md) +- [Authentication](authentication.md) diff --git a/doc/authentication.md b/doc/authentication.md new file mode 100644 index 0000000..a0c6036 --- /dev/null +++ b/doc/authentication.md @@ -0,0 +1,34 @@ +# Authentication + +Webfuse supports two authentications mechanisms: + +- token-based authentication using HTTP headers +- in-protocol authentication + +To activate authentication, two command line option can be specified: + +- `--wf-authenticator PATH` + allows to specify an executable used for authentication +- `--wf-auth-header HEADER` _(optional)_ + allows to specify the HTTP header used for authentication + +When `--wf-auth-header` is not specifiend or the header is not contained +in the HTTP request, the in-protocol solutions is used: Before any other +operation, the credentials are queried via `getcreds`request. + +## Authenticator + +An authenticator is an executable or script used for token-based +authentication. Credentials are passed to the authenticator via `stdin`. + +## Header restrictions + +Note that not any HTTP header can be specified using `--wf-auth-header` +option. The following headers are supported: + +- `X-Auth-Token` +- `Authorization` + +In addition to that, any non-standard header can be specified. + +Due to implementation limitation, most standard headers can not be used by now. Please let us know, when you intend to use a header that is not supported yet. Please create an issue in that case. diff --git a/doc/build.md b/doc/build.md new file mode 100644 index 0000000..2f40343 --- /dev/null +++ b/doc/build.md @@ -0,0 +1,35 @@ +# webfuse build instructions + +## Build + +```` +cmake -B build +cmake --build build +sudo cmake --install build +```` + +## Build options + +| Options | Default | Description | +| ------------------ | -------- | ----------- | +| WITHOUT_PROVIDER | OFF | Disables build of webfuse provider | +| WITHOUT_TEST | OFF | Disables build of unit and integration tests | +| WITHOUT_CLANG_TIDY | OFF | Disables running clang tidy on build | + +## Dependencies + +- [libfuse](https://github.com/libfuse/libfuse) +- [libwebsockets](https://libwebsockets.org/) + +### Optional dependencies + +- [googletest](https://github.com/google/googletest) +- [valgrind](https://valgrind.org/) +- [clang-tidy](https://clang.llvm.org/extra/clang-tidy/) + +## Additional cmake targets + +| Target | Description | +| -------- | ----------- | +| test | runs unit and integration tests | +| memcheck | runs unit test with valgrind/memcheck | diff --git a/doc/concept.png b/doc/concept.png new file mode 100644 index 0000000..bf5b875 Binary files /dev/null and b/doc/concept.png differ diff --git a/doc/concept.uml b/doc/concept.uml new file mode 100644 index 0000000..eb2737a --- /dev/null +++ b/doc/concept.uml @@ -0,0 +1,26 @@ +@startuml +participant "webfuse provider" as provider +participant "webfuse service" as service +actor "user" as user + +group startup +service -> service : fuse_mount +service -> service : start websocket server +end +... + +group connect +provider -> service : connect +end +... + + +group directory listing +user -> service : ls +service -> provider : readdir request +provider --> service : readdir response +service --> user : [., ..] +end +... + +@enduml diff --git a/doc/protocol.md b/doc/protocol.md new file mode 100644 index 0000000..ac4322a --- /dev/null +++ b/doc/protocol.md @@ -0,0 +1,916 @@ +# Webfuse2 Protocol + +## Scope + +This document describes the webfuse 2 communication protocol. The protocol is used to transfer messages between a `webfuse service` and a `webfuse provider`. In contrast to `legacy webfuse`, which is based on `JSON RPC` the `webfuse2 protocol` is a binary protocol. + +## Definitions + +### Webfuse Service + +A `webfuse service` is both, +- a [websocket](https://en.wikipedia.org/wiki/WebSocket) service providing the `webfuse` protocol +- a [fuse](https://github.com/libfuse/libfuse) filesystem attached to a local mountpoint + +The `webfuse service` awaits incoming connections from a `webfuse provider`. Once connected, it communicates all the filesystem requests originated by the `libfuse` to the connected `webfuse provider` using the `websocket`-based `webfuse protocol`. + +By doing so, `webfuse` allows to inject a filesystem to a remote device. + +### Webfuse Provider + +A `webfuse provider` provides a filesystem to a remote device using the `websocket`-based `webfuse protocol`. Therefore, a `webfuse provider` implements a `websocket` client. + +## Websocket protocol name + +The webfuse2 protocol uses the following websocket protocol name: `webfuse2`. + +## Message exchange + +Once connected, the `webfuse2 protocol` implements a strict request-response scheme, where +- all requests are send by the `webfuse service`, +- all requests require a response and +- all responses are send by the `webfuse provider` + +Note that this communication is reversed the typical client-server-communication scheme. In `webfuse` the `webfuse service` (server) sends all the requests and the `webfuse provider` (client) sends the responses. + +For message transfer, the [websocket](https://en.wikipedia.org/wiki/WebSocket) protocol is used. All messages are in binary form, plain text messages are never used by the `webfuse protocol`. + + +## Endianness + +All numeric data types are transferred in [Big Endian](https://en.wikipedia.org/wiki/Endianness). +For instance, the uint32 value 1 will be transferred as + + 00 00 00 01 + +## Data Types + +### Basic data types + +| Data Type | Width | Description | +| --------- | ------ | ----------- | +| bool | 8 bit | boolean value | +| u8 | 8 bit | 8 bit unsigned integer | +| u32 | 32 bit | 32 bit unsigned integer | +| u64 | 64 bit | 64 bit unsigned integer | +| i32 | 32 bit | 32 bit signed integer | + +### Derrived integral types + +| Data Type | Base Type | Description | +| --------- | --------- | ----------- | +| uid | u32 | user ID | +| gid | u32 | group ID | +| dev | u64 | device ID | +| handle | u64 | file handle | + +### Flags and Enums + +| Data Type | Base Type | Description | +| ------------ | --------- | ------------------------- | +| result | i32 | result type of methods | +| access_mode | u32 | mode of `access` method | +| mode | u32 | file type and permissions | +| open_flags | i32 | flags of `open` method | +| rename_flags | u8 | flags of `rename` method | + +#### result + +| Value Range | Description | +| -------------- | ---------------------- | +| 0 | method call succeed | +| negative value | error code (see below) | +| positive value | amount of bytes read or written (`read` and `write` only) | + +| Error Code | Value | Description | +| ------------ | ----- | ----------- | +| E2BIG | -7 | argument list too long | +| EACCES | -13 | permission denied | +| EAGAIN | -11 | resource temporarily unavailable | +| EBADF | -9 | bad file descriptor | +| EBUSY | -16 | device or resource busy | +| EDESTADDRREQ | -89 | destination address required | +| EDQUOT | -122 | disk quota exceeded | +| EEXIST | -17 | file exists | +| EFAULT | -14 | bad address | +| EFBIG | -27 | file too large | +| EINTR | -4 | interrupt function call | +| EINVAL | -22 | invalid argument | +| EIO | -5 | input / output error | +| EISDIR | -21 | is a directory +| ELOOP | -40 | too many levels of symbolic links | +| EMFILE | -24 | too many open files | +| EMLINK | -31 | too many links | +| ENAMETOOLONG | -36 | filename too long | +| ENFILE | -23 | too many open files in system | +| ENODATA | -61 | the named attribute does not exist, or the process has not access to this attribute | +| ENODEV | -19 | no such device | +| ENOENT | -2 | no such file or directory | +| ENOMEM | -12 | not enough space / cannot allocate memory | +| ENOSPC | -28 | no space left on device | +| ENOSYS | -38 | function not implemented | +| ENOTDIR | -20 | not a directory | +| ENOTEMPTY | -39 | directory not empty | +| ENOTSUP | -95 | operation not supported | +| ENXIO | -6 | no such device or address | +| EOVERFLOW | -75 | value too large to be stored in data type | +| EPERM | -1 | operation not permitted | +| EPIPE | -32 | broken pipe | +| ERANGE | -34 | result too large | +| EROFS | -30 | read-only filesystem | +| ETXTBSY | -26 | text file busy | +| EXDEV | -18 | improper link | +| EWOULDBLOCK | -11 | resource temporarily unavailable | + +#### access mode + +| Mode | Value | Description | +| ---- | ----- | ----------- | +| F_OK | 0 | Tests, whether the file exists | +| X_OK | 1 | Tests, whether the file is executable | +| W_OK | 2 | Tests, whether the file is writable | +| R_OK | 4 | Tests, whether the file is readable | + +#### mode + +_Note that the following numbers are in `octal` notation._ + +| Fields and Flags | Mask | Description | +| ---------------- | -------- | ----------- | +| Protection mask | 0o000777 | Cointains the file protection flags (rwx for owner, group and others) | +| Sticky mask | 0o007000 | Sticky bits | +| S_ISVTX | 0o001000 | Sticky bit | +| S_ISGID | 0o002000 | Set-Group-ID bit | +| S_ISUID | 0o004000 | Set-User-ID bit | +| Filetype mask | 0o170000 | Filetype mask | +| S_IFREG | 0o100000 | regular file | +| S_IFDIR | 0o040000 | directory | +| S_IFCHR | 0o020000 | character device | +| S_IFBLK | 0o060000 | block device | +| S_IFIFO | 0o010000 | named pipe | +| S_IFLNK | 0o120000 | link | +| S_IFSOCK | 0o140000 | socket | + +#### open_flags + +_Note that the following numbers are in `octal` notation._ + +| Flags | Value | Description | +| ---------------- | -------- | ----------- | +| O_ACCMODE | 0o03 | Access mode mask | +| O_RDONLY | 0o00 | open file read-only | +| O_WRONLY | 0o01 | open file write-only | +| O_RDWR | 0o02 | open file for reading and writing | +| O_APPEND | 0o000002000 | open file in append mode | +| O_ASYNC | 0o000020000 | enable signal-driven I/O | +| O_CLOEXEC | 0o002000000 | enable close-on-exec | +| O_CREAT | 0o000000100 | create file if path does not exists | +| O_DIRECT | 0o000040000 | try to minimize cache effects on I/O | +| O_DIRECTORY | 0o000200000 | open a directory | +| O_DSYNC | 0o000010000 | write with synchronized I/O data integrity | +| O_EXCL | 0o000000200 | ensure that file exists when specified in conjunction with O_CREATE | +| O_LARGEFILE | 0o000100000 | allow large files to be opened | +| O_NOATIME | 0o001000000 | do not update file last access time | +| O_NOCTTY | 0o000000400 | make device the process's controlling terminal | +| O_NOFOLLOW | 0o000400000 | fail to open, if basename is a symbolic link | +| O_NONBLOCK | 0o000004000 | open in nonblocking mode | +| O_NDELAY | 0o000004000 | open in nonblocking mode | +| O_PATH | 0o010000000 | see manual entry of `open(2)` for details | +| O_SYNC | 0o004010000 | write using synchronized I/O | +| O_TMPFILE | 0o020200000 | create an unnamed temporary file | +| O_TRUNC | 0o000001000 | truncate fole to length 0 | + +#### rename_flags + +| Flag | Value | Description | +| ---------------- | ----- | ----------- | +| RENAME_NOREPLACE | 1 | do not overwrite the new file | +| RENAME_EXCHANGE | 2 | atomically exchange the files | + +### Complex Types + +| Data Type | Description | +| ---------- | --------------------- | +| string | UTF-8 string | +| strings | list of strings | +| bytes | array of bytes | +| timestamp | date and time | +| attributes | file attributes | +| statistics | filesystem statistics | + +#### string + +| Field | Data Type | Description | +| ----- | --------- | ----------------------------- | +| size | u32 | length of the string in bytes | +| data | u8[] | string data (UTF-8) | + +#### strings + +| Field | Data Type | Description | +| ----- | --------- | --------------------------------- | +| size | u32 | count of the elements in the list | +| data | string[] | strings | + +#### bytes + +| Field | Data Type | Description | +| ----- | --------- | ------------------------ | +| size | u32 | length of the byte array | +| data | u8[] | array data | + +#### timestamp + +| Field | Data Type | Description | +| ------- | --------- | ----------------------------- | +| seconds | u64 | seconds sind epoch (1.1.1970) | +| nsec | u32 | nano seconds | + +#### attributes + +| Field | Data Type | Description | +| ------ | ---------- | -------------------------- | +| inode | u64 | Inode value | +| nlink | u64 | Number of hard links | +| mode | mode (u32) | file mode flags | +| uid | uid (u32) | user ID | +| gid | gid (u32) | group ID | +| rdev | dev (u64) | device ID | +| size | u64 | file size | +| blocks | u64 | number 512-byte blocks | +| atime | timestamp | time of last access | +| mtime | timestamp | time of last modification | +| ctime | timestamp | time of last status change | + +#### statistics + +| Field | Data Type | Description | +| ------ | ---------- | -------------------------- | +| bsize | u64 | Filesystem block size | +| frsize | u64 | Fragment size | +| blocks | u64 | Size of the filesystem if `frsize` units | +| bfree | u64 | Number of free blocks | +| bavail | u64 | Number of free blocks for unprivileged users | +| files | u64 | Number of inodes | +| ffree | u64 | Number of free inodes | +| namemax | u64 | Maximum filename length | + +## Messages + +| Field | Type | Descripton | +| ------- | ---- | ---------- | +| id | u32 | Unique ID of the message | +| type | u8 | Type of the message | +| payload | u8[] | Payload according to the message type | + +The `id` is just a number without any meaning for the `webfuse provider`. It is set by the `webfuse service` of a request and is copied by the `webfuse provider` to the response. A `webfuse service` implementation might choose to keep track on pending requests using the `id`. + +### Erroneous Responses + +Most responses contain a `result` encoding the status of the operation. While successful responses may contain additional data, erroneous responses must not be decoded by a `webfuse service` implementation beyond the `result` value. + +### Unknown requests + +There are two reserved message types: +- **0x00:** Unknown request +- **0x80:** Unknown response + +A `webfuse service` may send a request of type `unknown request` for conformance testing reasons. + +Since each request requires a response, a `webfuse provider` must respond to any unknown requests with a message of `unknown response` type. This allows to add new request types in future. + +### Accept additional data in requests + +Both, a `webfuse provider` and a `webfuse service` must accept messages that contain more data than specified. This allows to add optional fields to existing requests and / or responses in future. + +_Note there are no optional fields in the current revision of the `webfuse2 protocol` yet._ + +### Message Types + +_Note that the following numbers are in `hexadecimal` notation._ + +| Method | Request | Response | +| -------- | ------- | -------- | +| access | 0x01 | 0x81 | +| getattr | 0x02 | 0x82 | +| readlink | 0x03 | 0x83 | +| symlink | 0x04 | 0x84 | +| link | 0x05 | 0x85 | +| rename | 0x06 | 0x86 | +| chmod | 0x07 | 0x87 | +| chown | 0x08 | 0x88 | +| truncate | 0x09 | 0x89 | +| fsync | 0x0a | 0x8a | +| open | 0x0b | 0x8b | +| mknod | 0x0c | 0x8c | +| create | 0x0d | 0x8d | +| release | 0x0e | 0x8e | +| unlink | 0x0f | 0x8f | +| read | 0x10 | 0x90 | +| write | 0x11 | 0x91 | +| mkdir | 0x12 | 0x92 | +| readdir | 0x13 | 0x93 | +| rmdir | 0x14 | 0x94 | +| statfs | 0x15 | 0x95 | +| utimens | 0x16 | 0x96 | +| getcreds | 0x17 | 0x97 | + +## Methods + +Since `webfuse` aims to communicate the `libfuse API` over a `websocket` connection, `webfuse` methods are tightly connected to [fuse operations](https://libfuse.github.io/doxygen/structfuse__operations.html) which itself have a tight connection to `posix filesystem operations`. Therefore, additional information about most `webfuse` operations can be found in the [fuse operations documentation](https://libfuse.github.io/doxygen/structfuse__operations.html) and / or the [man pages](https://man7.org/index.html). + +### access + +Checks the user's permissions for a file (see [man access(2)](https://man7.org/linux/man-pages/man2/access.2.html)). + +#### Request + +| Field | Data Type | Description | +| ----- | ---------------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x01) | +| path | string | path of file to check | +| mode | access_mode (i8) | access mode to check | + +#### Response + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x81) | +| result | result | operation status | + +### getattr + +Retrieve file attributes (see [man getattr(2)](https://man7.org/linux/man-pages/man2/getxattr.2.html)). + +#### Request + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x02) | +| path | string | path | + + +#### Response + +| Field | Data Type | Description | +| --------- | ---------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x82) | +| result | result | operation status | +| attibutes | attributes | attributes of file | + +### readlink + +Read value of a symbolik link (see [man readlink(2)](https://man7.org/linux/man-pages/man2/readlink.2.html)). + +#### Request + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x03) | +| path | string | path of link | + + +#### Response + +| Field | Data Type | Description | +| -------- | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x83) | +| result | result | operation status | +| resolved | string | resolved path | + +### symlink + +Make a new name of a file (see [man symlink(2)](https://man7.org/linux/man-pages/man2/symlink.2.html)). + +#### Request + +| Field | Data Type | Description | +| -------- | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x04) | +| target | string | target of link | +| linkpath | string | name of the link | + +#### Response + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x84) | +| result | result | operation status | + +### link + +Make a new name for a file (see [man link(2)](https://man7.org/linux/man-pages/man2/link.2.html)). + +#### Request + +| Field | Data Type | Description | +| -------- | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x05) | +| old_path | string | path of the existing file | +| new_path | string | new name of the file | + +#### Response + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x85) | +| result | result | operation status | + +### rename + +Change the name of a file (see [man rename(2)](https://man7.org/linux/man-pages/man2/rename.2.html)). + +#### Request + +| Field | Data Type | Description | +| -------- | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x06) | +| old_path | string | old name of the file | +| new_path | string | new name of the file | +| flags | rename_flags (u8) | flags to control the rename operation | + +The following `flags` are defined: +- **0x00:** move the file from `old_path` to `new_path` +- **0x01 (RENAME_NOREPLACE):** do not override `new_path` + This results in an error, when `new_path` already exists. +- **0x02 (RENAME_EXCHANGE):** atomically exchange the files + +#### Response + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x86) | +| result | result | operation status | + +### chmod + +Change permissions of a file (see [man chmod(2)](https://man7.org/linux/man-pages/man2/chmod.2.html)). + +#### Request + +| Field | Data Type | Description | +| ------ | ---------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x07) | +| path | string | path of the file | +| mode | mode (u32) | new file permissions | + +#### Response + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x87) | +| result | result | operation status | + +### chown + +Change ownership of a file (see [man chown(2)](https://man7.org/linux/man-pages/man2/chown.2.html)). + +#### Request + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x08) | +| path | string | path of the file | +| uid | uid (u32) | user id of the new owner | +| gid | gid (u32) | group id of the new owning group | + +#### Response + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x88) | +| result | result | operation status | + +### truncate + +Truncate a file to a specified length (see [man truncate(2)](https://man7.org/linux/man-pages/man2/truncate64.2.html)). + +#### Request + +| Field | Data Type | Description | +| ------ | ------------ | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x09) | +| path | string | path of the file | +| size | u64 | new file size | +| handle | handle (u64) | handle of the file | + +_Note that handle might be invalid (-1), even if the file is open._ + +#### Response + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x89) | +| result | result | operation status | + +### fsync + +Sychronize a file's in-core state with storage device (see [man fsync(2)](https://man7.org/linux/man-pages/man2/fsync.2.html)). + +#### Request + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x0a) | +| path | string | path of the file | +| is_datasync | bool | if true, sync only user data | +| handle | handle (u64) | handle of the file | + +#### Response + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x8a) | +| result | result | operation status | + +### open + +Open and possibly create a file ([man open(2)](https://man7.org/linux/man-pages/man2/open.2.html)). + +#### Request + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x0b) | +| path | string | path of the file | +| flags | open_flags (i32) | flags | + +#### Response + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x8b) | +| result | result | operation status | +| handle | handle (u64) | handle of the file | + +### mknod + +Create a special or ordinary file (see [man mknod(2)](https://man7.org/linux/man-pages/man2/mknod.2.html)). + +#### Request + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x0c) | +| path | string | path of the file | +| mode | mode (u32) | mode of the file | +| dev | dev (64) | device type | + +#### Response + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x8c) | +| result | result | operation status | + +### create + +Create a new file or rewrite an existing one (see [man creat(3p)](https://man7.org/linux/man-pages/man3/creat.3p.html)). + +#### Request + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x0d) | +| path | string | path of the file | +| mode | mode (u32) | mode of the file | + +#### Response + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x8d) | +| result | result | operation status | +| handle | handle (u64) | handle of the file | + +### release + +Releases a file handle (see [man close(2)](https://man7.org/linux/man-pages/man2/close.2.html)). + +#### Request + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x0e) | +| path | string | path of the file | +| handle | handle (u64) | handle of the file | + +#### Response + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x8e) | +| result | result | operation status | + +### unlink + +Delete a name and possibly the file it refers to ([man unlink(2)](https://man7.org/linux/man-pages/man2/unlink.2.html)). + +#### Request + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x0f) | +| path | string | path of the file | + +#### Response + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x8f) | +| result | result | operation status | + +### read + +Read from a file description (see [man read(2)](https://man7.org/linux/man-pages/man2/read.2.html), [man pread(2)](https://man7.org/linux/man-pages/man2/pread.2.html)). + +#### Request + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x10) | +| path | string | path of the file | +| buffer_size | u32 | max. amount of bytes requested | +| offset | u64 | offset of the file | +| handle | handle (u64) | handle of the file | + +#### Response + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x90) | +| result | result | amount of byte read or error code | +| data | bytes | requested data | + +_Note that results returns the amount of bytes read on success._ + +### write + +Write to a file (see [man write(2)](https://man7.org/linux/man-pages/man2/write.2.html), [man pread(2)](https://man7.org/linux/man-pages/man2/pread.2.html)). + +#### Request + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x17) | +| data | bytes | data to write | +| offset | u64 | offset to write to | +| handle | handle (u64) | handle of the file | + +#### Response + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x91) | +| result | result | amount of bytes written or error code | + +_Note that results returns the amount of bytes written on success._ + +### mkdir + +Create a directory (see [man mkdir(2)](https://man7.org/linux/man-pages/man2/mkdir.2.html)). + +#### Request + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x12) | +| path | string | path of the directory | +| mode | mode (u32) | directory permissions | + +#### Response + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x92) | +| result | result | operation status | + +### readdir + +Reads the contents of a directory. + +#### Request + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x13) | +| path | string | path of the directory | + +#### Response + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x93) | +| result | result | operation status | +| items | strings | names of the directory entries | + +### rmdir + +Delete a directory (see [man rmdir(2)](https://man7.org/linux/man-pages/man2/rmdir.2.html)). + +#### Request + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x14) | +| path | string | path of the directory | + +#### Response + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x94) | +| result | result | operation status | + +### statfs + +Get filesystem statistics (see [man statvfs(3)](https://man7.org/linux/man-pages/man3/statvfs.3.html)). + +#### Request + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x15) | +| path | string | path of the file | + +#### Response + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x95) | +| result | result | operation status | +| statistics | statistics | filesystem statistics | + +### utimens + +Change the file timestamps with nanosecond precision ([man utimesat(2)](https://man7.org/linux/man-pages/man2/utimensat.2.html)). + +#### Request + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x16) | +| path | string | path of the file | +| atime | timestamp | new last access time | +| mtime | timestamp | new last modified time | +| handle | handle (u64) | handle of the file | + +_Note that handle might be invalid (-1), even if the file is open._ + +#### Response + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x96) | +| result | result | operation status | + +### getcreds + +Query credentials. When authentication is active and the in-protocol +authentication mechanism is used, this is the first request a +webfuse service sends to a provider. + +#### Request + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x17) | + +_Note that handle might be invalid (-1), even if the file is open._ + +#### Response + +| Field | Data Type | Description | +| ------ | --------- | ----------- | +| id | u32 | message id | +| type | u8 | message type (0x97) | +| creds | str | credentials | + +## Examples + +### Get file attributes + +```` +service -> provider: + 00 00 00 01 # message id = 1 + 02 # message type = getattr request + 00 00 00 01 # path.size = 1 + '/' # path = "/" + +provider -> service: + 00 00 00 01 # message id = 1 + 82 # message type = getattr response + 00 00 00 00 # result = 0 (OK) + 00 00 00 00 00 00 00 01 # attributes.inode = 1 + 00 00 00 00 00 00 00 02 # attributes.nlink = 2 + 00 00 41 a4 # attributes.mode = 0o40644 (S_IDDIR | 0o0644) + 00 00 03 e8 # attributes.uid = 1000 + 00 00 03 e8 # attributes.gid = 1000 + 00 00 00 00 00 00 00 00 # attributes.size = 0 + 00 00 00 00 00 00 00 00 # attributes.blocks = 0 + 00 00 00 00 00 00 00 00 # attrbites.atime.sec = 0 + 00 00 00 00 00 # attributs.atime.nsec = 0 + 00 00 00 00 00 00 00 00 # attrbites.mtime.sec = 0 + 00 00 00 00 00 # attributs.mtime.nsec = 0 + 00 00 00 00 00 00 00 00 # attrbites.ctime.sec = 0 + 00 00 00 00 00 # attributs.ctime.nsec = 0 +```` + +### Get file attributes (Failure) + +_Note that attributs are skipped in case of an error._ + +```` +service -> provider: + 00 00 00 01 # message id = 1 + 02 # message type = getattr request + 00 00 00 04 # path.size = 4 + "/foo" # path = "/foo" + +provider -> service: + 00 00 00 01 # message id = 1 + 82 # message type = getattr response + ff ff ff fe # result = -2 (ENOENT) +```` + +### List directory contents + +_Note that '.' and '..' should not be included in the response._ + +```` +service -> provider: + 00 00 00 02 # message id = 2 + 13 # message type = readdir request + 00 00 00 04 # path.size = 4 + '/dir' # path = "/dir" + +provider -> service: + 00 00 00 02 # message id = 2 + 93 # message type = readdir response + 00 00 00 00 # result = 0 (OK) + 00 00 00 03 # items.size = 3 + 00 00 00 03 # items[0].size = 3 + "foo" # items[0] = "foo" + 00 00 00 03 # items[0].size = 3 + "bar" # items[0] = "bar" + 00 00 00 03 # items[0].size = 3 + "baz" # items[0] = "baz" +```` + +### Unknown request + +```` +service -> provider: + 00 00 00 23 # message id = 0x23 + 42 # message type = ??? (not specified yet) + ... # some more data + +provider -> service: + 00 00 00 23 # message id = 0x23 + 80 # message type = unknown response +```` diff --git a/doc/webfuse.md b/doc/webfuse.md new file mode 100644 index 0000000..34cca0f --- /dev/null +++ b/doc/webfuse.md @@ -0,0 +1,79 @@ +# webfuse command line options + +In order to inject a remote filesystem, webfuse mounts a local +filesystem via fuse and exposes it's API via websockets. + +## Usage + + webfuse [options] <mountpoint> + +## Webfuse specific options + +| Option | Argument | Default | Description | +| ------------ | -------- | --------- | ----------- | +| --wf-port | port | 8081 | Specify the port of the websocket server | +| --wf-vhost | vhost | localhost | Specify the name of the websocket server's virtual host | +| --wf-cert | path | - | Optional. Specify the file path of the server's public certificate | +| --wf-key | path | - | Optional. Specify the file path of the server's private key | +| --wf-authenticator | path | - | Optional. Specify the file path of the authenticator executable | +| --wf-auth-header | name | - | Optional. Specify the name of the HTTP header used for authentication | +| --wf-docroot | path | - | Optional. Enabled HTTP server with given document root. | +| --wf-timeout | timeout | 10 | Optional. Specify the communication timeout. | +| --wf-version | - | - | Print version and exit. | + +**Note:** All paths must be absolute _(this might be relaxed if future versions)_. + +## Fuse options + +| Option | Descripion | +| --------------------- | ---------- | +| -h, --help | print help | +| -V --version | print version | +| -d -o debug | enable debug output (implies -f) | +| -f | foreground operation | +| -s | disable multi-threaded operation | +| -o clone_fd | use separate fuse device fd for each thread | +| | (may improve performance) | +| -o max_idle_threads | the maximum number of idle worker threads | +| | allowed (default: 10) | +| -o kernel_cache | cache files in kernel | +| -o [no]auto_cache | enable caching based on modification times (off) | +| -o umask=M | set file permissions (octal) | +| -o uid=N | set file owner | +| -o gid=N | set file group | +| -o entry_timeout=T | cache timeout for names (1.0s) | +| -o negative_timeout=T | cache timeout for deleted names (0.0s) | +| -o attr_timeout=T | cache timeout for attributes (1.0s) | +| -o ac_attr_timeout=T | auto cache timeout for attributes (attr_timeout) | +| -o noforget | never forget cached inodes | +| -o remember=T | remember cached inodes for T seconds (0s) | +| -o modules=M1[:M2...] | names of modules to push onto filesystem stack | +| -o allow_other | allow access by all users | +| -o allow_root | allow access by root | +| -o auto_unmount | auto unmount on process termination | + +### Options for subdir module + +| Option | Descripion | +| --------------------- | ---------- | +| -o subdir=DIR | prepend this directory to all paths (mandatory) | +| -o [no]rellinks | transform absolute symlinks to relative | + +### Options for iconv module + +| Option | Descripion | +| --------------------- | ---------- | +| -o from_code=CHARSET | original encoding of file names (default: UTF-8) | +| -o to_code=CHARSET | new encoding of the file names (default: UTF-8) | + +## Examples + +- run webfuse in foreground on default port: + `webfuse -f /path/to/mointpoint` +- run webfuse in forground on port 8080: + `webfuse -f --wf-port 8080 /path/to/mountpoint` +- run webfuse using TLS: + `webfuse -f --wf-cert /path/to/cert --wf-key /path/to/key /path/to/mountpoint` +- run webfuse using authentication via `X-Auth-Token` header: + `webfuse -f --wf-authenticator /path/to/authenticator \` + ` --wf-auth-header X-Auth-Token /path/to/mountpoint` diff --git a/doc/webfuse_provider.md b/doc/webfuse_provider.md new file mode 100644 index 0000000..7ce47dd --- /dev/null +++ b/doc/webfuse_provider.md @@ -0,0 +1,39 @@ +# webfuse_provider command line options + +Inject a remote filesystem via webfuse. + +## Usage + + webfuse_provider -u [-p ] [-a ] + +## Options + +| Short Option | Long Option | Argument | Description | +| ------------ | ----------- | -------- | ----------- | +| -h | --help | - | print usage and exit | +| -v | --version | - | print version an exit | +| -p | --path | path | path of local filesystem to inject (default: .) | +| -u | --url | url | url of webfuse server | +| -a | --ca-path | path | path of ca file | +| -t | --token | token | authentication token (overrides WEBFUSE_TOKEN) | + +## Environment variables + +| Variable | Description | +| -------- | ----------- | +| WEBFUSE_TOKEN | Default value of webfuse token | + +## Examples + +- inject current directory: + `webfuse_provider -u ws://localhost/` +- inject a given directory: + `webfuse_provider -u ws://localhost/ -p /path/to/directory` +- inject current directory to port 8080: + `webfuse_provider -u ws://localhost:8080/` +- inject current directory via TLS: + `webfuse_provider -u wss://localhost/` +- inject current diectory via TLS using a specific ca: + `webfuse_provider -u wss://localhost/ -a /path/to/server-cert.pem` +- inject current directory, authenticate via token: + `webfuse_provider -u wss://localhost/ -t my_token` diff --git a/example/authenticator/pam/.gitignore b/example/authenticator/pam/.gitignore new file mode 100644 index 0000000..84c048a --- /dev/null +++ b/example/authenticator/pam/.gitignore @@ -0,0 +1 @@ +/build/ diff --git a/example/authenticator/pam/CMakeLists.txt b/example/authenticator/pam/CMakeLists.txt new file mode 100644 index 0000000..6f46a4b --- /dev/null +++ b/example/authenticator/pam/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.10) +project(webfuse VERSION 2.0.0) + +option(WITHOUT_CLANG_TIDY "Disables clang tidy" OFF) + +set (CMAKE_CXX_STANDARD 17) + +find_library(PAM pam REQUIRED) +find_library(B64 b64 REQUIRED) + +add_executable(webfuse_pam_authenticator src/main.cpp) +target_link_libraries(webfuse_pam_authenticator pam b64) +install(TARGETS webfuse_pam_authenticator DESTINATION bin) +install(FILES etc/pam.d/webfuse DESTINATION /etc/pam.d) + +add_executable(webfuse_pam_token_encode src/token_encode.cpp) +target_link_libraries(webfuse_pam_token_encode b64) + +add_executable(webfuse_pam_token_decode src/token_decode.cpp) +target_link_libraries(webfuse_pam_token_decode b64) + +if(NOT(WITHOUT_CLANG_TIDY)) +set_property( + TARGET webfuse_pam_authenticator webfuse_pam_token_encode webfuse_pam_token_decode + PROPERTY CXX_CLANG_TIDY clang-tidy -checks=readability-*,-readability-identifier-length -warnings-as-errors=*) +endif() diff --git a/example/authenticator/pam/README.md b/example/authenticator/pam/README.md new file mode 100644 index 0000000..b89da3e --- /dev/null +++ b/example/authenticator/pam/README.md @@ -0,0 +1,31 @@ +# webfuse PAM authenticator + +This directory contains an example of a webfuse authenticator using PAM. + +The authenticator uses `username` and `password` for authentication. +Since webfuse only provides a token, username and password are encoded as follows: + + TOKEN := base64 ( USERNAME ":" PASSWORD ) + +Example: + + USERNAME := "user" + PASSWORD := "secret" + TOKEN := base64 ( "user:secret" ) = "XNlcjpzZWNyZXQ=" + +The utilities `webfuse_pam_token_encode` and `webfuse_pam_token_decode` can be used +to encode and decode tokens. + +## Build + + cmake -b build + cmake build + +## Dependencies + +- libpam +- libb64 + +## Notes + +- in order to make the authenticator work, read access to /etc/shadow is needed diff --git a/example/authenticator/pam/etc/pam.d/webfuse b/example/authenticator/pam/etc/pam.d/webfuse new file mode 100644 index 0000000..593775d --- /dev/null +++ b/example/authenticator/pam/etc/pam.d/webfuse @@ -0,0 +1,7 @@ +# Use unix authentication for webfuse authentication. +# +# NOTE: +# - in order to use unix authentication, read access to /etc/shadow is required + +auth required pam_unix.so +account required pam_unix.so diff --git a/example/authenticator/pam/src/main.cpp b/example/authenticator/pam/src/main.cpp new file mode 100644 index 0000000..3a6c2aa --- /dev/null +++ b/example/authenticator/pam/src/main.cpp @@ -0,0 +1,181 @@ +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace +{ + +bool decode(std::string const & token, std::string & username, std::string & password) +{ + std::istringstream encoded(token); + std::stringstream decoded; + + base64::decoder decoder; + decoder.decode(encoded, decoded); + auto const plain = decoded.str(); + auto const pos = plain.find(':'); + bool const success = (pos != std::string::npos); + if (success) + { + username = plain.substr(0, pos); + password = plain.substr(pos + 1); + } + + return success; +} + +struct credentials +{ + std::string username; + std::string password; +}; + +extern "C" int conversation( + int count, + struct pam_message const ** messages, + struct pam_response ** ret_responses, + void * user_data) +{ + if (count <= 0) { return PAM_CONV_ERR; } + + int result = PAM_SUCCESS; + auto * responses = reinterpret_cast(malloc(count * sizeof(struct pam_response))); + auto * creds = reinterpret_cast(user_data); + + for(int i = 0; (PAM_SUCCESS == result) && (i < count); i++) + { + auto * response = &responses[i]; + auto const * message = messages[i]; + + response->resp_retcode = 0; + response->resp = nullptr; + + switch(message->msg_style) + { + case PAM_PROMPT_ECHO_ON: + response->resp = strdup(creds->username.c_str()); + break; + case PAM_PROMPT_ECHO_OFF: + response->resp = strdup(creds->password.c_str()); + break; + case PAM_TEXT_INFO: + break; + case PAM_ERROR_MSG: + break; + default: + free(responses); + result = PAM_CONV_ERR; + break; + } + } + + if (PAM_SUCCESS == result) + { + *ret_responses = responses; + } + + return result; +} + +bool authenticate(std::string const & username, std::string const & password) +{ + credentials creds; + creds.username = username; + creds.password = password; + + struct pam_conv conv; + conv.conv = &conversation; + conv.appdata_ptr = reinterpret_cast(&creds); + + pam_handle_t * handle = nullptr; + bool cleanup_handle = false; + bool result = true; + { + auto const rc = pam_start("webfuse", nullptr, &conv, &handle); + result = (PAM_SUCCESS == rc); + cleanup_handle = result; + if (!result) + { + syslog(LOG_AUTH, "failed to start PAM conversation"); + } + } + + if (result) + { + pam_set_item(handle, PAM_USER, reinterpret_cast(username.c_str())); + auto const rc = pam_authenticate(handle, PAM_DISALLOW_NULL_AUTHTOK); + result = (PAM_SUCCESS == rc); + } + + if (result) + { + auto const rc = pam_acct_mgmt(handle, PAM_DISALLOW_NULL_AUTHTOK); + result = (PAM_SUCCESS == rc); + } + + if (cleanup_handle) + { + pam_end(handle, 0); + } + + return result; +} + +} + +int main(int argc, char* argv[]) +{ + int exit_code = EXIT_FAILURE; + + if (argc == 1) + { + std::string token; + std::getline(std::cin, token); + + openlog("webfuse_pam_auth", 0, LOG_AUTH); + + std::string username; + std::string password; + auto const decode_valid = decode(token, username, password); + if (decode_valid) + { + auto const is_authenticated = authenticate(username, password); + if (is_authenticated) + { + syslog(LOG_AUTH, "authenticate user \"%s\"", username.c_str()); + exit_code = EXIT_SUCCESS; + } + else + { + syslog(LOG_AUTH, "failed to authenticate user \"%s\"", username.c_str()); + } + } + + closelog(); + } + else + { + std::cout << R"(webfuse_pam_authenticator, (c) 2023 Falk Werner +webfuse PAM authenticator + +Usage: + webfuse_pam_authenticator [-h] + +Options: + --help, -h print this message and exit + +Credentials: + Credentials are passed as based64-encoded token via stdin: + token := base64( ":" ) +)"; + } + + return exit_code; +} \ No newline at end of file diff --git a/example/authenticator/pam/src/token_decode.cpp b/example/authenticator/pam/src/token_decode.cpp new file mode 100644 index 0000000..8fa38df --- /dev/null +++ b/example/authenticator/pam/src/token_decode.cpp @@ -0,0 +1,47 @@ +#include + +#include +#include +#include + +int main(int argc, char* argv[]) +{ + int exit_code = EXIT_FAILURE; + + if (argc == 2) + { + std::istringstream token(argv[1]); + + base64::decoder decoder; + std::stringstream plain; + decoder.decode(token, plain); + + auto const plain_str = plain.str(); + auto const pos = plain_str.find(':'); + if (pos != std::string::npos) + { + auto const username = plain_str.substr(0, pos); + auto const password = plain_str.substr(pos + 1); + + std::cout << "username: " << username << std::endl; + std::cout << "password: " << password << std::endl; + exit_code = EXIT_SUCCESS; + } + + if (exit_code != EXIT_SUCCESS) + { + std::cerr << "error: failed to decode" << std::endl; + } + } + else + { + std::cout << R"(webfuse_pam_token_encode, (c) 2023 Falk Werner + Encode token for webfuse PAM authenticator + + Usage: + webfuse_pam_token_encode +)"; + } + + return exit_code; +} \ No newline at end of file diff --git a/example/authenticator/pam/src/token_encode.cpp b/example/authenticator/pam/src/token_encode.cpp new file mode 100644 index 0000000..3b930fd --- /dev/null +++ b/example/authenticator/pam/src/token_encode.cpp @@ -0,0 +1,32 @@ +#include + +#include +#include +#include + +int main(int argc, char* argv[]) +{ + if (argc == 3) + { + std::string const username = argv[1]; + std::string const password = argv[2]; + + base64::encoder encoder; + std::istringstream plain(username + ':' + password); + std::stringstream token; + encoder.encode(plain, token); + + std::cout << token.str(); + } + else + { + std::cout << R"(webfuse_pam_token_encode, (c) 2023 Falk Werner + Encode token for webfuse PAM authenticator + + Usage: + webfuse_pam_token_encode +)"; + } + + return EXIT_SUCCESS; +} \ No newline at end of file diff --git a/example/authenticator/simple/authenticator.sh b/example/authenticator/simple/authenticator.sh new file mode 100755 index 0000000..5330483 --- /dev/null +++ b/example/authenticator/simple/authenticator.sh @@ -0,0 +1,13 @@ +#!/usr/bin/bash + +read AUTH_TOKEN + +if [[ "$AUTH_TOKEN" == "simple_token" ]] +then + echo "$(date): webfuse: auth granted: $AUTH_TOKEN" >> /tmp/webfuse_auth.log +else + echo "$(date): webfuse: auth denied: $AUTH_TOKEN" >> /tmp/webfuse_auth.log + exit 1 +fi + + diff --git a/example/provider/javascript/README.md b/example/provider/javascript/README.md new file mode 100644 index 0000000..629ac37 --- /dev/null +++ b/example/provider/javascript/README.md @@ -0,0 +1,6 @@ +# Webfuse JavaScript example + + mkdir -p /tmp/test + webfuse -f /tmp/test --wf-docroot . + +Visit [http://localhost:8081/](http://localhost:8081/). diff --git a/example/provider/javascript/index.html b/example/provider/javascript/index.html new file mode 100644 index 0000000..588f949 --- /dev/null +++ b/example/provider/javascript/index.html @@ -0,0 +1,37 @@ + + + + Webfuse Example + + + + +

Webfuse Example

+ +

+ This example provides a single file "README.md", which + contents can be altered below. +

+ +

Connection

+

+ + +

+

+ + +

+

+ + disconnected +

+ +

README.md

+

+ +

+ + \ No newline at end of file diff --git a/example/provider/javascript/js/filesystem.js b/example/provider/javascript/js/filesystem.js new file mode 100644 index 0000000..d571d5c --- /dev/null +++ b/example/provider/javascript/js/filesystem.js @@ -0,0 +1,104 @@ + +import { BaseFileSystem, ERRNO, Mode, AccessMode, OpenFlags } from "./webfuse/webfuse.js" + +class FileSystem extends BaseFileSystem { + + constructor(tokenProvider, stateListener, files) { + super(); + + this.tokenProvider = tokenProvider; + this.stateListener = stateListener + this.files = new Map(); + for(const file of files) { + this.files.set("/" + file.name, file); + } + } + + access(path, mode) { + // we do not allow write or execute + if ((mode & AccessMode.W_OK) || (mode & AccessMode.X_OK)) { + return ERRNO.EACCES; + } + + if ((path = "/") || (this.files.has(path))) { + return 0; + } + + return ERRNO.ENOENT; + } + + getattr(path) { + if (path == "/") { + return { + nlink: 2, + mode: Mode.DIR | 0o555 + }; + } + else if (this.files.has(path)) { + const file = this.files.get(path); + const contents = file.contents(); + return { + nlink: 1, + mode: Mode.REG | 0o444, + size: contents.length + }; + } + + return ERRNO.ENOENT; + } + + readdir(path) { + if (path == "/") { + const list = []; + for(const file of this.files.values()) { + list.push(file.name); + } + return list; + } + + return ERRNO.ENOENT; + } + + open(path, flags) { + if (this.files.has(path)) { + const accessMode = flags & OpenFlags.ACCESS_MODE; + if (accessMode == OpenFlags.RDONLY) { + return [0, 0]; + } + else { + return [ERRNO.EPERM, 0]; + } + } + + return [ERRNO.ENOENT, 0]; + } + + read(path, size, offset, fd) { + if (this.files.has(path)) { + const file = this.files.get(path); + const contents = file.contents(); + if (offset < contents.length) { + const available = contents.length - offset; + const length = (size < available) ? size : available; + const data = contents.slice(offset, offset + length); + return data; + } + + return []; + } + else { + return ERRNO.EBADF; + } + } + + getcreds() { + const token = this.tokenProvider(); + return token; + } + + connectionstatechanged(state) { + this.stateListener(state); + } +} + +export { FileSystem } diff --git a/example/provider/javascript/js/startup.js b/example/provider/javascript/js/startup.js new file mode 100644 index 0000000..576224e --- /dev/null +++ b/example/provider/javascript/js/startup.js @@ -0,0 +1,47 @@ + +import { Webfuse } from "./webfuse/webfuse.js"; +import { FileSystem } from "./filesystem.js"; + + +function encode(value) { + const encoder = new TextEncoder('utf-8'); + return encoder.encode(value); +} + +function get_contents() { + const contentTextArea = document.querySelector("#contents"); + const contents = contentTextArea.value; + return encode(contents); +} + +function get_token() { + const tokenTextfield = document.querySelector('#token'); + const token = tokenTextfield.value; + return token; +} + +function update_state(state) { + const stateTextField = document.querySelector("#state"); + stateTextField.textContent = (state == "connected") ? "connected" : "disconnected"; +} + +let webfuse = null; +const filesystem = new FileSystem(get_token, update_state, [ + {name: "README.md", contents: get_contents } +]); + +function onConnectButtonClicked() { + if (webfuse) { webfuse.close(); } + + const urlTextfield = document.querySelector('#url'); + const url = urlTextfield.value; + + webfuse = new Webfuse(url, filesystem); +} + +function startup() { + const connectButton = document.querySelector('#connect'); + connectButton.addEventListener('click', onConnectButtonClicked); +} + +document.addEventListener('DOMContentLoaded', startup(),false); diff --git a/example/provider/javascript/js/webfuse/accessmode.js b/example/provider/javascript/js/webfuse/accessmode.js new file mode 100644 index 0000000..dca6300 --- /dev/null +++ b/example/provider/javascript/js/webfuse/accessmode.js @@ -0,0 +1,9 @@ + +const AccessMode = { + F_OK: 0, + R_OK: 4, + W_OK: 2, + X_OK: 1 +}; + +export { AccessMode } diff --git a/example/provider/javascript/js/webfuse/basefilesystem.js b/example/provider/javascript/js/webfuse/basefilesystem.js new file mode 100644 index 0000000..1a68d87 --- /dev/null +++ b/example/provider/javascript/js/webfuse/basefilesystem.js @@ -0,0 +1,102 @@ +import { ERRNO } from "./errno.js" + +class BaseFileSystem { + + access(path, mode) { + return ERRNO.ENOENT; + } + + getattr(path) { + return ERRNO.ENOENT; + } + + readlink(path) { + return ERRNO.ENOENT; + } + + symlink(target, linkpath) { + return ERRNO.ENOENT; + } + + link(oldpath, newpath) { + return ERRNO.ENOENT; + } + + rename(oldpath, newpath, flags) { + return ERRNO.ENOENT; + } + + chmod(path, mode) { + return ERRNO.EPERM; + } + + chown(path, uid, gid) { + return ERRNO.EPERM; + } + + truncate(path, size, fd) { + return ERRNO.EPERM; + } + + fsync(path, isDataSync, fd) { + return 0; + } + + open(path, flags) { + return [ERRNO.ENOENT, 0]; + } + + mknod(path, mode, rdev) { + return ERRNO.EPERM; + } + + create(path, mode) { + return [ERRNO.EPERM, 0]; + } + + release(path, fd) { + return 0; + } + + unlink(path) { + return ERRNO.EPERM; + } + + read(path, size, offset, fd) { + return ERRNO.EBADF; + } + + write(path, data, offset, fd) { + return ERRNO.EBADF; + } + + mkdir(path, mode) { + return ERRNO.EPERM; + } + + readdir(path) { + return ERRNO.ENOENT; + } + + rmdir(path) { + return ERRNO.EPERM; + } + + statfs(path) { + return ERRNO.ENOSYS; + } + + utimens(path, atime, mtime) { + return ERRNO.ENOSYS; + } + + getcreds() { + return ""; + } + + connectionstatechanged(state) { + // pass + } +} + +export { BaseFileSystem } diff --git a/example/provider/javascript/js/webfuse/errno.js b/example/provider/javascript/js/webfuse/errno.js new file mode 100644 index 0000000..1c6047b --- /dev/null +++ b/example/provider/javascript/js/webfuse/errno.js @@ -0,0 +1,40 @@ +const ERRNO = { + E2BIG : -7, + EACCES : -13, + EAGAIN : -11, + EBADF : -9, + EBUSY : -16, + EDESTADDRREQ : -89, + EDQUOT : -122, + EEXIST : -17, + EFAULT : -14, + EFBIG : -27, + EINTR : -4, + EINVAL : -22, + EIO : -5, + EISDIR : -21, + ELOOP : -40, + EMFILE : -24, + EMLINK : -31, + ENAMETOOLONG : -36, + ENFILE : -23, + ENODATA : -61, + ENODEV : -19, + ENOENT : -2, + ENOMEM : -12, + ENOSPC : -28, + ENOSYS : -38, + ENOTDIR : -20, + ENOTEMPTY : -39, + ENOTSUP : -95, + ENXIO : -6, + EOVERFLOW : -75, + EPERM : -1, + EPIPE : -32, + ERANGE : -34, + EROFS : -30, + ETXTBSY : -26, + EXDEV : -18 +}; + +export { ERRNO } \ No newline at end of file diff --git a/example/provider/javascript/js/webfuse/messagereader.js b/example/provider/javascript/js/webfuse/messagereader.js new file mode 100644 index 0000000..737564a --- /dev/null +++ b/example/provider/javascript/js/webfuse/messagereader.js @@ -0,0 +1,58 @@ +class MessageReader { + + constructor(data) { + this.raw = data; + this.data = new DataView(data); + this.pos = 0; + this.decoder = new TextDecoder('utf-8'); + } + + read_u8() { + const result = this.data.getUint8(this.pos); + this.pos++; + return result; + } + + read_bool() { + return this.read_u8() == 1; + } + + read_u32() { + const result = this.data.getUint32(this.pos); + this.pos += 4; + return result; + } + + read_u64() { + const result = this.data.getBigUint64(this.pos); + this.pos += 8; + return Number(result); + } + + read_str() { + const length = this.read_u32(); + if (length > 0) { + const view = new Uint8Array(this.raw, this.pos, length); + this.pos += length; + return this.decoder.decode(view); + } + else { + return ""; + } + } + + read_bytes() { + const length = this.read_u32(); + if (length > 0) { + const view = new Uint8Array(this.raw, this.pos, length); + this.pos += length; + return view; + } + else { + return []; + } + } + +} + +export { MessageReader } \ No newline at end of file diff --git a/example/provider/javascript/js/webfuse/messagewriter.js b/example/provider/javascript/js/webfuse/messagewriter.js new file mode 100644 index 0000000..3c5a9f9 --- /dev/null +++ b/example/provider/javascript/js/webfuse/messagewriter.js @@ -0,0 +1,75 @@ +class MessageWriter { + + constructor(message_id, message_type) { + this.data = [ ] + this.write_u32(message_id) + this.write_u8(message_type) + this.encoder = new TextEncoder("utf-8"); + } + + write_u8(value) { + this.data.push(value) + } + + write_u32(value) { + const buffer = new ArrayBuffer(4); + const view = new DataView(buffer); + view.setUint32(0, value); + + const data = new Uint8Array(buffer); + this.data.push(...data); + } + + write_i32(value) { + const buffer = new ArrayBuffer(4); + const view = new DataView(buffer); + view.setInt32(0, value); + + const data = new Uint8Array(buffer); + this.data.push(...data); + } + + write_u64(value) { + const buffer = new ArrayBuffer(8); + const view = new DataView(buffer); + view.setBigUint64(0, BigInt(value)); + + const data = new Uint8Array(buffer); + this.data.push(...data); + } + + // value in milliseconds + write_time(value) { + const seconds = Math.floor(value / 1000); + const millis = value % 1000; + const nanos = millis * 1000 * 1000; + + this.write_u64(seconds); + this.write_u32(nanos); + } + + write_str(value) { + const data = this.encoder.encode(value); + this.write_u32(data.length); + this.data.push(...data); + } + + write_strings(list) { + this.write_u32(list.length); + for(const item of list) { + this.write_str(item); + } + } + + write_bytes(value) { + this.write_u32(value.length); + this.data.push(...value); + } + + get_data() { + return new Uint8Array(this.data); + } + +} + +export { MessageWriter } \ No newline at end of file diff --git a/example/provider/javascript/js/webfuse/openflags.js b/example/provider/javascript/js/webfuse/openflags.js new file mode 100644 index 0000000..b63badf --- /dev/null +++ b/example/provider/javascript/js/webfuse/openflags.js @@ -0,0 +1,28 @@ + +const OpenFlags = { + ACCESS_MODE: 0x03, + RDONLY : 0o00, + WRONLY : 0o01, + RDWR : 0o02, + + APPEND : 0o00002000, + ASYNC : 0o00020000, + CLOEXEC : 0o02000000, + CREAT : 0o00000100, + DIRECT : 0o00040000, + DIRECTORY : 0o00200000, + DSYNC : 0o00010000, + EXCL : 0o00000200, + LARGEFILE : 0o00100000, + NOATIME : 0o01000000, + NOCTTY : 0o00000400, + NOFOLLOW : 0o00400000, + NONBLOCK : 0o00004000, + NDELAY : 0o00004000, + PATH : 0o10000000, + SYNC : 0o04010000, + TMPFILE : 0o20200000, + TRUNC : 0o00001000 +}; + +export { OpenFlags } \ No newline at end of file diff --git a/example/provider/javascript/js/webfuse/webfuse.js b/example/provider/javascript/js/webfuse/webfuse.js new file mode 100644 index 0000000..c8e49de --- /dev/null +++ b/example/provider/javascript/js/webfuse/webfuse.js @@ -0,0 +1,309 @@ +import { MessageWriter } from "./messagewriter.js"; +import { MessageReader } from "./messagereader.js"; +import { ERRNO } from "./errno.js"; +import { AccessMode } from "./accessmode.js"; +import { OpenFlags } from "./openflags.js"; +import { BaseFileSystem } from "./basefilesystem.js"; + + +const Mode = { + REG : 0o100000, + DIR : 0o040000, + CHR : 0o020000, + BLK : 0o060000, + FIFO : 0o010000, + LNK : 0o120000, + SOCK : 0o140000 +}; + + + +function fs_access(reader, writer, filesystem) { + const path = reader.read_str(); + const mode = reader.read_u8(); + const result = filesystem.access(path, mode); + writer.write_i32(result); +} + +function fs_getattr(reader, writer, filesystem) { + const path = reader.read_str(); + const result = filesystem.getattr(path); + if (typeof(result) !== "number") { + writer.write_i32(0); + writer.write_u64(result.ino | 0); + writer.write_u64(result.nlink | 0); + writer.write_u32(result.mode | 0); + writer.write_i32(result.uid | 0); + writer.write_i32(result.gid | 0); + writer.write_u64(result.dev | 0); + writer.write_u64(result.size | 0); + writer.write_u64(result.blocks | 0); + writer.write_time(result.atime | 0); + writer.write_time(result.mtime | 0); + writer.write_time(result.ctime | 0); + } + else { + writer.write_i32(result); + } +} + +function fs_readlink(reader, writer, filesystem) { + const path = reader.read_str(); + const result = filesystem.readlink(path); + if (typeof(result) != "number") { + writer.write_i32(0); + writer.write_str(result); + } + else { + writer.write_i32(result); + } +} + +function fs_symlink(reader, writer, filesystem) { + const target = reader.read_str(); + const linkpath = reader.read_str(); + const result = filesystem.symlink(target, linkpath); + writer.write_i32(result); +} + + +function fs_link(reader, writer, filesystem) { + const oldpath = reader.read_str(); + const newpath = reader.read_str(); + const result = filesystem.link(oldpath, newpath); + writer.write_i32(result); +} + +function fs_rename(reader, writer, filesystem) { + const oldpath = reader.read_str(); + const newpath = reader.read_str(); + const flags = reader.read_u8(); + const result = filesystem.rename(oldpath, newpath, flags); + writer.write_i32(result); +} + +function fs_chmod(reader, writer, filesystem) { + const path = reader.read_str(); + const mode = reader.read_u32(); + const result = filesystem.chmod(path, mode); + writer.write_i32(result); +} + +function fs_chown(reader, writer, filesystem) { + const path = reader.read_str(); + const uid = reader.read_u32(); + const gid = reader.read_u32(); + const result = filesystem.chown(path, uid, gid); + writer.write_i32(result); +} + +function fs_truncate(reader, writer, filesystem) { + const path = reader.read_str(); + const size = reader.read_u64(); + const fd = reader.read_u64(); + const result = filesystem.truncate(path, size, fd); + writer.write_i32(result); +} + +function fs_fsync(reader, writer, filesystem) { + const path = reader.read_str(); + const isDataSync = reader.read_bool(); + const fd = reader.read_fd(); + const result = filesystem.fsync(path, isDataSync, fd); + writer.write_i32(result); +} + +function fs_open(reader, writer, filesystem) { + const path = reader.read_str(); + const flags = reader.read_u32(); + const [result, fd] = filesystem.open(path, flags); + writer.write_i32(result); + writer.write_u64(fd); +} + +function fs_mknod(reader, writer, filesystem) { + const path = reader.read_str(); + const mode = reader.read_u32(); + const rdev = reader.read_u64(); + const result = filesystem.mknod(path, mode, rdev); + writer.write_i32(result); +} + +function fs_create(reader, writer, filesystem) { + const path = reader.read_str(); + const mode = reader.read_u32(); + const [result, fd] = filesystem.create(path, mode); + writer.write_i32(result); + writer.write_u64(fd); +} + +function fs_release(reader, writer, filesystem) { + const path = reader.read_str(); + const fd = reader.read_u64(); + const result = filesystem.release(path, fd); + writer.write_i32(result); +} + +function fs_unlink(reader, writer, filesystem) { + const path = reader.read_str(); + const result = filesystem.unlink(path); + writer.write_i32(result); +} + +function fs_read(reader, writer, filesystem) { + const path = reader.read_str(); + const size = reader.read_u32(); + const offset = reader.read_u64(); + const fd = reader.read_u64(); + const result = filesystem.read(path, size, offset, fd); + if (typeof(result) != "number") { + writer.write_i32(result.length); + writer.write_bytes(result); + } + else { + writer.write_i32(result); + } +} + +function fs_write(reader, wriuter, filesystem) { + const path = reader.read_str(); + const data = reader.read_bytes(); + const offset = reader.read_u64(); + const fd = reader.read_u64(); + const result = filesystem.write(path, data, offset, fd); + writer.write_i32(result); +} + +function fs_mkdir(reader, writer, filesystem) { + const path = reader.read_str() + const mode = reader.read_u32(); + const result = filesystem.mkdir(path, mode); + writer.write_i32(result); +} + +function fs_readdir(reader, writer, filesystem) { + const path = reader.read_str(); + const result = filesystem.readdir(path); + if (typeof(result) != "number") { + writer.write_i32(0); + writer.write_strings(result); + } + else { + writer.write_i32(result); + } +} + +function fs_rmdir(reader, writer, filesystem) { + const path = reader.read_str(); + const result = filesystem.rmdir(path); + writer.write_i32(result); +} + +function fs_statfs(reader, writer, filesystem) { + const path = reader.read_str(); + const result = filesystem.statfs(path); + if (typeof(result) != "number") { + writer.write_i32t(0) + writer.write_u64(result.bsize | 0); + writer.write_u64(result.frsize | 0); + writer.write_u64(result.blocks | 0); + writer.write_u64(result.bfree | 0); + writer.write_u64(result.bavail | 0); + writer.write_u64(result.files | 0); + writer.write_u64(result.ffree | 0); + writer.write_u64(result.namemax | 0); + } + else { + writer.write_i32(result); + } +} + +function fs_utimens(reader, writer, filesystem) { + const path = reader.read_str(); + const atime = reader.read_time(); + const mtime = reader.read_time(); + const result = filesystem.utimens(path, atime, mtime); + writer.write_i32(result); +} + +function fs_getcreds(reader, writer, filesystem) { + const credentials = filesystem.getcreds(); + writer.write_str(credentials); +} + +const commands = new Map([ + [0x01, fs_access], + [0x02, fs_getattr], + [0x03, fs_readlink], + [0x04, fs_symlink], + [0x05, fs_link], + [0x06, fs_rename], + [0x07, fs_chmod], + [0x08, fs_chown], + [0x09, fs_truncate], + [0x0a, fs_fsync], + [0x0b, fs_open], + [0x0c, fs_mknod], + [0x0d, fs_create], + [0x0e, fs_release], + [0x0f, fs_unlink], + [0x10, fs_read], + [0x11, fs_write], + [0x12, fs_mkdir], + [0x13, fs_readdir], + [0x14, fs_rmdir], + [0x15, fs_statfs], + [0x16, fs_utimens], + [0x17, fs_getcreds] +]); + +class Webfuse { + + constructor(url, filesystem) { + this.ws = new WebSocket(url, ["webfuse2"]); + this.ws.binaryType = 'arraybuffer'; + this.ws.addEventListener('open', (event) => this.on_connected(event)); + this.ws.addEventListener('close', (event) => this.on_closed(event)); + this.ws.addEventListener('error', (event) => this.on_error(event)); + this.ws.addEventListener('message', (event) => this.on_message(event)); + + this.filesystem = filesystem; + } + + close() { + this.ws.close(); + } + + on_message(event) { + const reader = new MessageReader(event.data); + const message_id = reader.read_u32(); + const message_type = reader.read_u8(); + + const writer = new MessageWriter(message_id, 0x80 + message_type); + if (commands.has(message_type)) { + const command = commands.get(message_type); + command(reader, writer, this.filesystem); + } + else { + console.warn(`unknow message type: ${message_type}`); + } + + this.ws.send(writer.get_data()); + } + + on_connected(event) { + this.filesystem.connectionstatechanged("connected"); + } + + on_error(event) { + console.info("connection error"); + this.ws.close(); + } + + on_closed(event) { + this.filesystem.connectionstatechanged("closed"); + } + +} + +export { Webfuse, BaseFileSystem, ERRNO, Mode, AccessMode, OpenFlags } diff --git a/example/provider/javascript/style.css b/example/provider/javascript/style.css new file mode 100644 index 0000000..52ee0d5 --- /dev/null +++ b/example/provider/javascript/style.css @@ -0,0 +1,28 @@ +html, body { + background-color: #c0c0c0; +} + +h1 { + background-color: black; + color: white; +} + +label { + display: inline-block; + width: 150px; +} + +input { + width: 150px; +} + +#stats { + display: inline-block; + text-align: right; + width: 150px; +} + +textarea { + width: 300px; + height: 300px; +} \ No newline at end of file diff --git a/example/provider/python/pylintrc b/example/provider/python/pylintrc new file mode 100644 index 0000000..61caec8 --- /dev/null +++ b/example/provider/python/pylintrc @@ -0,0 +1,17 @@ +[BASIC] + +good-names=i,j,k,ex,Run,_,f,fd + +[MESSAGES CONTROL] + + +disable=raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + missing-function-docstring + diff --git a/example/provider/python/requirements.txt b/example/provider/python/requirements.txt new file mode 100644 index 0000000..2da5042 --- /dev/null +++ b/example/provider/python/requirements.txt @@ -0,0 +1 @@ +websockets==10.4 diff --git a/example/provider/python/webfuse_provider.py b/example/provider/python/webfuse_provider.py new file mode 100755 index 0000000..d508a29 --- /dev/null +++ b/example/provider/python/webfuse_provider.py @@ -0,0 +1,600 @@ +#!/usr/bin/env python3 + +"""Example webfuse provider written in python.""" + +import asyncio +import os +import stat +import errno +import getpass +import argparse +import websockets + +INVALID_FD = 0xffffffffffffffff + +F_OK = 0 +R_OK = 4 +W_OK = 2 +X_OK = 1 + +RESPONSE = 0x80 + +ERRNO = { + -errno.E2BIG : -7, + -errno.EACCES : -13, + -errno.EAGAIN : -11, + -errno.EBADF : -9, + -errno.EBUSY : -16, + -errno.EDESTADDRREQ : -89, + -errno.EDQUOT : -122, + -errno.EEXIST : -17, + -errno.EFAULT : -14, + -errno.EFBIG : -27, + -errno.EINTR : -4, + -errno.EINVAL : -22, + -errno.EIO : -5, + -errno.EISDIR : -21, + -errno.ELOOP : -40, + -errno.EMFILE : -24, + -errno.EMLINK : -31, + -errno.ENAMETOOLONG : -36, + -errno.ENFILE : -23, + -errno.ENODATA : -61, + -errno.ENODEV : -19, + -errno.ENOENT : -2, + -errno.ENOMEM : -12, + -errno.ENOSPC : -28, + -errno.ENOSYS : -38, + -errno.ENOTDIR : -20, + -errno.ENOTEMPTY : -39, + -errno.ENOTSUP : -95, + -errno.ENXIO : -6, + -errno.EOVERFLOW : -75, + -errno.EPERM : -1, + -errno.EPIPE : -32, + -errno.ERANGE : -34, + -errno.EROFS : -30, + -errno.ETXTBSY : -26, + -errno.EXDEV : -18 +} + +RENAME_NOREPLACE = 0x01 +RENAME_EXCHANGE = 0x02 + +MODE_REG = 0o100000 +MODE_DIR = 0o040000 +MODE_CHR = 0o020000 +MODE_BLK = 0o060000 +MODE_FIFO = 0o010000 +MODE_LNK = 0o120000 +MODE_SOCK = 0o140000 + +O_RDONLY = 0o00 +O_WRONLY = 0o01 +O_RDWR = 0o02 + +O_APPEND = 0o00002000 +O_ASYNC = 0o00020000 +O_CLOEXEC = 0o02000000 +O_CREAT = 0o00000100 +O_DIRECT = 0o00040000 +O_DIRECTORY = 0o00200000 +O_DSYNC = 0o00010000 +O_EXCL = 0o00000200 +O_LARGEFILE = 0o00100000 +O_NOATIME = 0o01000000 +O_NOCTTY = 0o00000400 +O_NOFOLLOW = 0o00400000 +O_NONBLOCK = 0o00004000 +O_NDELAY = 0o00004000 +O_PATH = 0o10000000 +O_SYNC = 0o04010000 +O_TMPFILE = 0o20200000 +O_TRUNC = 0o00001000 + +class MessageReader: + """Reads webfuse messages from buffer.""" + + def __init__(self, buffer): + self.buffer = buffer + self.offset = 0 + + def read_u8(self): + value = self.buffer[self.offset] + self.offset += 1 + return value + + def read_bool(self): + return self.read_u8() == 1 + + def read_u32(self): + value = (self.buffer[self.offset] << 24) + (self.buffer[self.offset + 1] << 16) + \ + (self.buffer[self.offset + 2] << 8) + self.buffer[self.offset + 3] + self.offset += 4 + return value + + def read_u64(self): + value = ( + (self.buffer[self.offset ] << 56) + + (self.buffer[self.offset + 1] << 48) + + (self.buffer[self.offset + 2] << 40) + + (self.buffer[self.offset + 3] << 32) + + (self.buffer[self.offset + 4] << 24) + + (self.buffer[self.offset + 5] << 16) + + (self.buffer[self.offset + 6] << 8) + + self.buffer[self.offset + 7]) + self.offset += 8 + return value + + + def read_str(self): + return self.read_bytes().decode() + + def read_bytes(self): + size = self.read_u32() + value = self.buffer[self.offset : self.offset + size] + self.offset += size + return value + + def read_path(self, base_path): + local_path = self.read_str().lstrip('/') + return os.path.join(base_path, local_path) + + def read_access_mode(self): + value = self.read_u8() + mode = os.F_OK if F_OK == (value & F_OK) else 0 + mode |= os.R_OK if R_OK == (value & R_OK) else 0 + mode |= os.W_OK if W_OK == (value & W_OK) else 0 + mode |= os.X_OK if X_OK == (value & X_OK) else 0 + return mode + + def read_rename_flags(self): + return self.read_u8() + + def read_mode(self): + value = self.read_u32() + mode = value & 0o7777 + mode |= stat.S_IFREG if MODE_REG == (value & MODE_REG ) else 0 + mode |= stat.S_IFDIR if MODE_DIR == (value & MODE_DIR ) else 0 + mode |= stat.S_IFCHR if MODE_CHR == (value & MODE_CHR ) else 0 + mode |= stat.S_IFBLK if MODE_BLK == (value & MODE_BLK ) else 0 + mode |= stat.S_IFIFO if MODE_FIFO == (value & MODE_FIFO) else 0 + mode |= stat.S_IFLNK if MODE_LNK == (value & MODE_LNK ) else 0 + mode |= stat.S_IFSOCK if MODE_SOCK == (value & MODE_SOCK) else 0 + return mode + + def read_openflags(self): + value = self.read_u32() + flags = 0 + # Access Mode + flags |= os.O_RDONLY if O_RDONLY == (value & O_RDONLY) else 0 + flags |= os.O_WRONLY if O_WRONLY == (value & O_WRONLY) else 0 + flags |= os.O_RDWR if O_RDWR == (value & O_RDWR ) else 0 + # Flags + flags |= os.O_APPEND if O_APPEND == (value & O_APPEND ) else 0 + flags |= os.O_ASYNC if O_ASYNC == (value & O_ASYNC ) else 0 + flags |= os.O_CLOEXEC if O_CLOEXEC == (value & O_CLOEXEC ) else 0 + flags |= os.O_CREAT if O_CREAT == (value & O_CREAT ) else 0 + flags |= os.O_DIRECT if O_DIRECT == (value & O_DIRECT ) else 0 + flags |= os.O_DIRECTORY if O_DIRECTORY == (value & O_DIRECTORY) else 0 + flags |= os.O_DSYNC if O_DSYNC == (value & O_DSYNC ) else 0 + flags |= os.O_EXCL if O_EXCL == (value & O_EXCL ) else 0 + flags |= os.O_LARGEFILE if O_LARGEFILE == (value & O_LARGEFILE) else 0 + flags |= os.O_NOCTTY if O_NOCTTY == (value & O_NOCTTY ) else 0 + flags |= os.O_NOFOLLOW if O_NOFOLLOW == (value & O_NOFOLLOW ) else 0 + flags |= os.O_NONBLOCK if O_NONBLOCK == (value & O_NONBLOCK ) else 0 + flags |= os.O_NDELAY if O_NDELAY == (value & O_NDELAY ) else 0 + flags |= os.O_PATH if O_PATH == (value & O_PATH ) else 0 + flags |= os.O_SYNC if O_SYNC == (value & O_SYNC ) else 0 + flags |= os.O_TMPFILE if O_TMPFILE == (value & O_TMPFILE ) else 0 + flags |= os.O_TRUNC if O_TRUNC == (value & O_TRUNC ) else 0 + return flags + + + +class MessageWriter: + """"Writes webfuse messages into buffer.""" + + def __init__(self, message_id, message_type): + self.buffer = [] + self.write_u32(message_id) + self.write_u8(message_type) + + def write_u8(self, value): + self.buffer.append(value) + + def write_u32(self, value): + self.buffer.extend([ + (value >> 24) & 0xff, + (value >> 16) & 0xff, + (value >> 8) & 0xff, + value & 0xff + ]) + + def write_u64(self, value): + self.buffer.extend([ + (value >> 56) & 0xff, + (value >> 48) & 0xff, + (value >> 40) & 0xff, + (value >> 32) & 0xff, + (value >> 24) & 0xff, + (value >> 16) & 0xff, + (value >> 8) & 0xff, + value & 0xff + ]) + + def write_i32(self, value): + self.write_u32(value & 0xffffffff) + + def write_result(self, value): + if 0 > value: + value = ERRNO.get(value, value) + self.write_i32(value) + + def write_str(self, value): + data = value.encode('utf-8') + self.write_bytes(data) + + def write_bytes(self, value): + size = len(value) + self.write_u32(size) + self.buffer.extend(value) + + def write_strings(self, values): + count = len(values) + self.write_u32(count) + for value in values: + self.write_str(value) + + def get_bytes(self): + return bytearray(self.buffer) + +# pylint: disable=too-many-public-methods +class FilesystemProvider: + """Webfuse filesystem provider.""" + + def __init__(self, path, url, token): + self.root = os.path.abspath(path) + self.url = url + self.token = token + self.commands = { + 0x01: FilesystemProvider.access, + 0x02: FilesystemProvider.getattr, + 0x03: FilesystemProvider.readlink, + 0x04: FilesystemProvider.symlink, + 0x05: FilesystemProvider.link, + 0x06: FilesystemProvider.rename, + 0x07: FilesystemProvider.chmod, + 0x08: FilesystemProvider.chown, + 0x09: FilesystemProvider.truncate, + 0x0a: FilesystemProvider.fsync, + 0x0b: FilesystemProvider.open, + 0x0c: FilesystemProvider.mknod, + 0x0d: FilesystemProvider.create, + 0x0e: FilesystemProvider.release, + 0x0f: FilesystemProvider.unlink, + 0x10: FilesystemProvider.read, + 0x11: FilesystemProvider.write, + 0x12: FilesystemProvider.mkdir, + 0x13: FilesystemProvider.readdir, + 0x14: FilesystemProvider.rmdir, + 0x15: FilesystemProvider.statfs, + 0x16: FilesystemProvider.utimens, + 0x17: FilesystemProvider.getcreds, + } + + async def run(self): + extra_headers = [("X-Auth-Token", self.token)] if self.token != "" else [] + # pylint: disable-next=no-member + async with websockets.connect(self.url, extra_headers=extra_headers) as connection: + while True: + request = await connection.recv() + reader = MessageReader(request) + message_id = reader.read_u32() + message_type = reader.read_u8() + writer = MessageWriter(message_id, RESPONSE + message_type) + if message_type in self.commands: + method = self.commands[message_type] + method(self, reader, writer) + else: + print(f"unknown message type: {message_type}") + response = writer.get_bytes() + await connection.send(response) + + def access(self, reader, writer): + path = reader.read_path(self.root) + mode = reader.read_access_mode() + result = -errno.EACCES + try: + if os.access(path, mode) is True: + result = 0 + except OSError as ex: + result = -ex.errno + writer.write_result(result) + + def getattr(self, reader, writer): + path = reader.read_path(self.root) + try: + attr = os.lstat(path) + except OSError as ex: + writer.write_result(-ex.errno) + return + writer.write_result(0) + writer.write_u64(attr.st_ino) + writer.write_u64(attr.st_nlink) + writer.write_u32(attr.st_mode) + writer.write_i32(attr.st_uid) + writer.write_i32(attr.st_gid) + writer.write_u64(attr.st_dev) + writer.write_u64(attr.st_size) + writer.write_u64(attr.st_blocks) + writer.write_u64(int(attr.st_atime)) + writer.write_u32(attr.st_atime_ns) + writer.write_u64(int(attr.st_mtime)) + writer.write_u32(attr.st_mtime_ns) + writer.write_u64(int(attr.st_ctime)) + writer.write_u32(attr.st_ctime_ns) + + def readlink(self, reader, writer): + path = reader.read_path(self.root) + try: + link = os.readlink(path) + except OSError as ex: + writer.write_result(-ex.errno) + return + writer.write_result(0) + writer.write_str(link) + + def symlink(self, reader, writer): + source = reader.read_str() + target = reader.read_path(self.root) + result = 0 + try: + os.symlink(source, target) + except OSError as ex: + result = -ex.errno + writer.write_result(result) + + def link(self, reader, writer): + source = reader.read_path(self.root) + target = reader.read_path(self.root) + result = 0 + try: + os.link(source, target) + except OSError as ex: + result = -ex.errno + writer.write_result(result) + + def rename(self, reader, writer): + source = reader.read_path(self.root) + target = reader.read_path(self.root) + flags = reader.read_rename_flags() + result = 0 + try: + if RENAME_EXCHANGE == (flags & RENAME_EXCHANGE): + # exchange is not supported + result = -errno.EINVAL + elif RENAME_NOREPLACE == (flags & RENAME_NOREPLACE): + os.rename(source, target) + else: + os.replace(source, target) + except OSError as ex: + result = -ex.errno + writer.write_result(result) + + def chmod(self, reader, writer): + path = reader.read_path(self.root) + mode = reader.read_mode() + result = 0 + try: + os.chmod(path, mode) + except OSError as ex: + result = -ex.errno + writer.write_result(result) + + def chown(self, reader, writer): + path = reader.read_path(self.root) + uid = reader.read_u32() + gid = reader.read_u32() + result = 0 + try: + os.chown(path, uid, gid) + except OSError as ex: + result = -ex.errno + writer.write_result(result) + + def truncate(self, reader, writer): + path = reader.read_path(self.root) + size = reader.read_u64() + fd = reader.read_u64() + result = 0 + try: + if fd != INVALID_FD: + os.ftruncate(fd, size) + else: + os.truncate(path, size) + except OSError as ex: + result = -ex.errno + writer.write_result(result) + + def fsync(self, reader, writer): + _ = reader.read_path(self.root) + _ = reader.read_bool() + fd = reader.read_u64() + result = 0 + try: + os.fsync(fd) + except OSError as ex: + result = -ex.errno + writer.write_result(result) + + def utimens(self, reader, writer): + path = reader.read_path(self.root) + atime = reader.read_u64() + atime_ns = reader.read_u32() + mtime = reader.read_u64() + mtime_ns = reader.read_u32() + result = 0 + try: + os.utime(path, (atime, mtime), ns = (atime_ns, mtime_ns)) + except OSError as ex: + result = -ex.errno + writer.write_result(result) + + def open(self, reader, writer): + path = reader.read_path(self.root) + flags = reader.read_openflags() + try: + fd = os.open(path, flags) + except OSError as ex: + writer.write_result(-ex.errno) + return + writer.write_result(0) + writer.write_u64(fd) + + def mknod(self, reader, writer): + path = reader.read_path(self.root) + mode = reader.read_mode() + rdev = reader.read_u64() + result = 0 + try: + os.mknod(path, mode, rdev) + except OSError as ex: + result = -ex.errno + writer.write_result(result) + + def create(self, reader, writer): + path = reader.read_path(self.root) + mode = reader.read_mode() + try: + flags = os.O_CREAT | os.O_WRONLY | os.O_TRUNC + fd = os.open(path, flags, mode) + except OSError as ex: + writer.write_result(-ex.errno) + return + writer.write_result(0) + writer.write_u64(fd) + + def release(self, reader, writer): + _ = reader.read_path(self.root) + fd = reader.read_u64() + result = 0 + try: + os.close(fd) + except OSError as ex: + writer.write_result(-ex.errno) + return + writer.write_result(result) + + def unlink(self, reader, writer): + path = reader.read_path(self.root) + result = 0 + try: + os.unlink(path) + except OSError as ex: + result = -ex.errno + writer.write_result(result) + + def read(self, reader, writer): + path = reader.read_path(self.root) + size = reader.read_u32() + offset = reader.read_u64() + fd = reader.read_u64() + try: + if fd != INVALID_FD: + buffer = os.pread(fd, size, offset) + else: + with os.open(path, os.O_RDONLY) as f: + buffer = os.pread(f, size, offset) + writer.write_result(len(buffer)) + writer.write_bytes(buffer) + except OSError as ex: + writer.write_result(-ex.errno) + + def write(self, reader, writer): + path = reader.read_path(self.root) + data = reader.read_bytes() + offset = reader.read_u64() + fd = reader.read_u64() + result = 0 + try: + if fd != INVALID_FD: + result = os.pwrite(fd, data, offset) + else: + with os.open(path, os.O_WRONLY) as f: + result = os.pwrite(f, data, offset) + except OSError as ex: + result = -ex.errno + writer.write_result(result) + + def mkdir(self, reader, writer): + path = reader.read_path(self.root) + mode = reader.read_u32() + result = 0 + try: + os.mkdir(path, mode) + except OSError as ex: + result = -ex.errno + writer.write_result(result) + + def readdir(self, reader, writer): + path = reader.read_path(self.root) + names = [] + try: + with os.scandir(path) as entries: + for entry in entries: + names.append(entry.name) + except OSError as ex: + writer.write_result(-ex.errno) + return + writer.write_result(0) + writer.write_strings(names) + + def rmdir(self, reader, writer): + path = reader.read_path(self.root) + result = 0 + try: + os.rmdir(path) + except OSError as ex: + result = -ex.errno + writer.write_result(result) + + def statfs(self, reader, writer): + path = reader.read_path(self.root) + try: + buffer = os.statvfs(path) + except OSError as ex: + writer.write_result(-ex.errno) + return + writer.write_result(0) + writer.write_u64(buffer.f_bsize) + writer.write_u64(buffer.f_frsize) + writer.write_u64(buffer.f_blocks) + writer.write_u64(buffer.f_bfree) + writer.write_u64(buffer.f_bavail) + writer.write_u64(buffer.f_files) + writer.write_u64(buffer.f_ffree) + writer.write_u64(buffer.f_namemax) + + def getcreds(self, _, writer): + credentials = self.token if self.token != None and self.token != "" else getpass.getpass(prompt="credentials: ") + writer.write_str(credentials) + +def main(): + parser = argparse.ArgumentParser(prog='webfuse_provider') + parser.add_argument('-p', '--path', type=str, required=False, default='.', + help='path to provide (default: \".\")') + parser.add_argument('-u', '--url', type=str, required=True, + help='URL of webfuse server, e.g. \"ws://localhost:8081\"') + parser.add_argument('-t', '--token', type=str, required=False, default='', + help='authentication token (default: contents of environment variable WEBFUSE_TOKEN)') + args = parser.parse_args() + token = args.token if args.token != "" else os.environ.get('WEBFUSE_TOKEN') + provider = FilesystemProvider(args.path, args.url, token) + try: + asyncio.run(provider.run()) + except KeyboardInterrupt as _: + pass + +if __name__ == '__main__': + main() diff --git a/script/create_cert.sh b/script/create_cert.sh new file mode 100755 index 0000000..26115bb --- /dev/null +++ b/script/create_cert.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +openssl req -x509 -newkey rsa:4096 \ + -keyout server-key.pem \ + -out server-cert.pem \ + -days 365 -nodes -batch \ + -subj /CN=localhost diff --git a/src/main.cpp b/src/main.cpp index 4112f94..4467a6a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,7 @@ -#include +#include "webfuse/webfuse.hpp" int main(int argc, char* argv[]) { - return EXIT_SUCCESS; -} \ No newline at end of file + webfuse::app app; + return app.run(argc, argv); +} diff --git a/src/provider_main.cpp b/src/provider_main.cpp new file mode 100644 index 0000000..94199ae --- /dev/null +++ b/src/provider_main.cpp @@ -0,0 +1,511 @@ +#include "webfuse/provider.hpp" +#include "webfuse/version.hpp" + +#include +#include +#include +#include + +#include +#include +#include + +#include + +namespace +{ + +enum class command +{ + run, + show_help, + show_version +}; + +class context +{ +public: + context(int argc, char* argv[]) + : base_path(".") + , url("") + , cmd(command::run) + , exit_code() + { + char const * const webfuse_token = getenv("WEBFUSE_TOKEN"); + if (nullptr != webfuse_token) + { + token = webfuse_token; + } + + struct option const long_options[] = + { + {"path" , required_argument, nullptr, 'p'}, + {"url" , required_argument, nullptr, 'u'}, + {"ca-path", required_argument, nullptr, 'a'}, + {"token" , required_argument, nullptr, 't'}, + {"version", no_argument , nullptr, 'v'}, + {"help" , no_argument , nullptr, 'h'}, + {nullptr , 0 , nullptr, 0 } + }; + + optind = 0; + opterr = 0; + bool finished = false; + while (!finished) + { + int option_index = 0; + const int c = getopt_long(argc, argv, "p:u:a:t:vh", long_options, &option_index); + switch (c) + { + case -1: + finished = true; + break; + case 'p': + base_path = optarg; + break; + case 'u': + url = optarg; + break; + case 'a': + ca_path = optarg; + break; + case 't': + token = optarg; + break; + case 'h': + cmd = command::show_help; + break; + case 'v': + cmd = command::show_version; + break; + default: + std::cerr << "error: unknown option" << std::endl; + cmd = command::show_help; + exit_code = EXIT_FAILURE; + finished = true; + break; + } + } + + if ((cmd == command::run) && (url.empty())) + { + std::cerr << "error: missing url" << std::endl; + cmd = command::show_help; + exit_code = EXIT_FAILURE; + } + } + + std::string base_path; + std::string url; + std::string ca_path; + std::string token; + command cmd; + int exit_code; +}; + +void print_usage() +{ + std::cout << R"(webfuse2 provider, (c) 2022 by Falk Werner +expose a local directory via webfuse2 + +Usage: + webfuse_provider -u [-p ] [-a ] + +Options: + --url, -u set url of webfuse2 service + --path, -p set path of directory to expose (default: .) + --ca-path, -a set path of ca file (default: not set) + --token, -t set authentication token (default: see below) + --version, -v print version and quit + --help, -h print this message and quit + +Authentication Token: + When requested by webfuse server, the authentication token + is determined as follows: + - if present, use contents of environment variable WEBFUSE_TOKEN + - else if specified, use the contents fo the -t option + - else query user for authentication token + +Examples: + webfuse-provider -u ws://localhost:8080/ + webfuse-provider -u ws://localhost:8080/ -p /some/directory +)"; +} + +void print_version() +{ + std::cout << webfuse::get_version() << std::endl; +} + +static bool shutdown_requested = false; +void on_signal(int _) +{ + (void) _; + shutdown_requested = true; +} + +class filesystem: public webfuse::filesystem_i +{ +public: + filesystem(std::string const & base_path, std::string const & token) + : token_(token) + { + char buffer[PATH_MAX]; + char * resolved_path = ::realpath(base_path.c_str(), buffer); + if (nullptr == resolved_path) + { + throw std::runtime_error("failed to resolve path"); + } + + struct stat info; + int const rc = stat(resolved_path, &info); + if (!S_ISDIR(info.st_mode)) + { + throw std::runtime_error("path is not a directory"); + } + + base_path_ = resolved_path; + } + + ~filesystem() override + { + + } + + int access(std::string const & path, int mode) override + { + auto const full_path = get_full_path(path); + + auto const result = ::access(full_path.c_str(), mode); + return (result == 0) ? 0 : -errno; + } + + int getattr(std::string const & path, struct stat * attr) override + { + auto const full_path = get_full_path(path); + + auto const result = lstat(full_path.c_str(), attr); + return (result == 0) ? 0 : -errno; + } + + int readlink(std::string const & path, std::string & out) override + { + auto const full_path = get_full_path(path); + + char buffer[PATH_MAX]; + int const result = ::readlink(full_path.c_str(), buffer, PATH_MAX); + if ((0 <= result) && (result < PATH_MAX)) + { + buffer[result] = '\0'; + out = buffer; + } + + return (result >= 0) ? 0 : -errno; + } + + int symlink(std::string const & from, std::string const & to) override + { + auto const full_from = ('/' == from.at(0)) ? get_full_path(from) : from; + auto const full_to = get_full_path(to); + + int const result = ::symlink(full_from.c_str(), full_to.c_str()); + return (result == 0) ? 0 : -errno; + } + + int link(std::string const & old_path, std::string const & new_path) override + { + auto const from = get_full_path(old_path); + auto const to = get_full_path(new_path); + + int const result = ::link(from.c_str(), to.c_str()); + return (result == 0) ? 0 : -errno; + } + + int rename(std::string const & old_path, std::string const & new_path, int flags) override + { + auto const full_old = get_full_path(old_path); + auto const full_new = get_full_path(new_path); + + int const result = ::renameat2(-1, full_old.c_str(), -1, full_new.c_str(), flags); + return (result == 0) ? 0 : -errno; + } + + int chmod(std::string const & path, mode_t mode) override + { + auto const full_path = get_full_path(path); + + int const result = ::chmod(full_path.c_str(), mode); + return (result == 0) ? 0 : -errno; + } + + int chown(std::string const & path, uid_t uid, gid_t gid) override + { + auto const full_path = get_full_path(path); + + int const result = ::chown(full_path.c_str(), uid, gid); + return (result == 0) ? 0 : -errno; + } + + int truncate(std::string const & path, uint64_t size, uint64_t handle) override + { + auto const full_path = get_full_path(path); + + int result = 0; + if (handle == webfuse::invalid_handle) + { + result = ::truncate(full_path.c_str(), size); + } + else + { + result = ::ftruncate(static_cast(handle), size); + } + + return (result == 0) ? 0 : -errno; + } + + int fsync(std::string const & path, bool is_datasync, uint64_t handle) override + { + int result = 0; + if (handle != webfuse::invalid_handle) + { + if (!is_datasync) + { + result = ::fsync(static_cast(handle)); + } + else + { + result = ::fdatasync(static_cast(handle)); + } + } + // we do not sync files, which are not open + + return (result == 0) ? 0 : -errno; + } + + int utimens(std::string const &path, struct timespec const tv[2], uint64_t handle) override + { + int result = 0; + if (handle == webfuse::invalid_handle) + { + auto const full_path = get_full_path(path); + result = ::utimensat(-1, full_path.c_str(), tv, 0); + } + else + { + result = ::futimens(static_cast(handle), tv); + } + + return (result == 0) ? 0 : -errno; + } + + int open(std::string const & path, int flags, uint64_t & handle) override + { + auto const full_path = get_full_path(path); + int const fd = ::open(full_path.c_str(), flags); + if (0 <= fd) + { + handle = static_cast(fd); + } + + return (0 <= fd) ? 0 : -errno; + } + + int mknod(std::string const & path, mode_t mode, dev_t rdev) override + { + auto const full_path = get_full_path(path); + int const result = ::mknod(full_path.c_str(), mode, rdev); + + return (result == 0) ? 0 : -errno; + } + + int create(std::string const & path, mode_t mode, uint64_t & handle) override + { + auto const full_path = get_full_path(path); + int const fd = ::creat(full_path.c_str(), mode); + if (0 <= fd) + { + handle = static_cast(fd); + } + + return (0 <= fd) ? 0 : -errno; + } + + int release(std::string const & path, uint64_t handle) override + { + int result = 0; + if (handle != webfuse::invalid_handle) + { + result = ::close(static_cast(handle)); + } + + return (result == 0) ? 0 : -errno; + } + + int unlink(std::string const & path) override + { + auto const full_path = get_full_path(path); + int const result = ::unlink(full_path.c_str()); + + return (result == 0) ? 0 : -errno; + } + + int read(std::string const & path, char * buffer, size_t buffer_size, uint64_t offset, uint64_t handle) override + { + int result = -1; + if (handle != webfuse::invalid_handle) + { + auto const full_path = get_full_path(path); + int fd = ::open(full_path.c_str(), O_RDONLY); + if (0 <= fd) + { + result = ::pread(fd, buffer, buffer_size, offset); + ::close(fd); + } + } + else + { + result = ::pread(static_cast(handle), buffer, buffer_size, offset); + } + + return (result >= 0) ? result : -errno; + } + + int write(std::string const & path, char const * buffer, size_t buffer_size, uint64_t offset, uint64_t handle) override + { + int result = -1; + if (handle == webfuse::invalid_handle) + { + auto const full_path = get_full_path(path); + int fd = ::open(full_path.c_str(), O_WRONLY); + if (0 <= fd) + { + result = ::pwrite(fd, buffer, buffer_size, offset); + ::close(fd); + } + } + else + { + result = ::pwrite(static_cast(handle), buffer, buffer_size, offset); + + } + + return (result >= 0) ? result : -errno; + } + + int mkdir(std::string const & path, mode_t mode) override + { + auto const full_path = get_full_path(path); + int const result = ::mkdir(full_path.c_str(), mode); + + return (result == 0) ? 0 : -errno; + } + + int readdir(std::string const & path, std::vector & entries) override + { + auto const full_path = get_full_path(path); + + int result = 0; + DIR * directory = opendir(full_path.c_str()); + if (NULL != directory) + { + dirent * entry = ::readdir(directory); + while (entry != nullptr) + { + entries.push_back(std::string(entry->d_name)); + entry = ::readdir(directory); + } + closedir(directory); + } + else + { + result = -errno; + } + + return result; + } + + int rmdir(std::string const & path) override + { + auto const full_path = get_full_path(path); + int const result = ::rmdir(full_path.c_str()); + + return (result == 0) ? 0 : -errno; + } + + int statfs(std::string const & path, struct statvfs * statistics) override + { + auto const full_path = get_full_path(path); + int const result = ::statvfs(full_path.c_str(), statistics); + + return (result == 0) ? 0 : -errno; + } + + std::string get_credentials() override + { + return (!token_.empty()) ? token_ : getpass("credentials: "); + } + + +private: + std::string get_full_path(std::string const & path) + { + return base_path_ + path; + } + + std::string base_path_; + std::string token_; +}; + +} + + + +int main(int argc, char* argv[]) +{ + context ctx(argc, argv); + + switch (ctx.cmd) + { + case command::run: + try + { + signal(SIGINT, &on_signal); + signal(SIGTERM, &on_signal); + + filesystem fs(ctx.base_path, ctx.token); + webfuse::provider provider(fs, ctx.ca_path); + provider.set_connection_listener([](bool connected) { + if (!connected) + { + shutdown_requested = true; + } + }); + provider.connect(ctx.url); + while (!shutdown_requested) + { + provider.service(); + } + } + catch (std::exception const & ex) + { + std::cerr << "error: " << ex.what() << std::endl; + ctx.exit_code = EXIT_FAILURE; + } + catch (...) + { + std::cerr << "error: unspecified error" << std::endl; + ctx.exit_code = EXIT_FAILURE; + } + break; + case command::show_version: + print_version(); + break; + case command::show_help: + // fall-through + default: + print_usage(); + break; + } + + return ctx.exit_code; +} diff --git a/src/webfuse/filesystem.cpp b/src/webfuse/filesystem.cpp new file mode 100644 index 0000000..d326a70 --- /dev/null +++ b/src/webfuse/filesystem.cpp @@ -0,0 +1,429 @@ +#include "webfuse/filesystem.hpp" + +#include +#include +#include + +namespace webfuse +{ + +filesystem::filesystem(ws_server& server) +: proxy(server) +{ + +} + +filesystem::~filesystem() +{ + +} + +int filesystem::access(std::string const & path, int mode) +{ + try + { + messagewriter req(request_type::access); + req.write_str(path); + req.write_access_mode(mode); + auto reader = proxy.perform(std::move(req)); + return reader.read_result(); + } + catch(...) + { + return fallback.access(path, mode); + } +} + +int filesystem::getattr(std::string const & path, struct stat * attr) +{ + try + { + messagewriter req(request_type::getattr); + req.write_str(path); + auto reader = proxy.perform(std::move(req)); + int const result = reader.read_result(); + if (0 == result) + { + reader.read_attr(attr); + } + return result; + } + catch(...) + { + return fallback.getattr(path, attr); + } +} + +int filesystem::readlink(std::string const & path, std::string & out) +{ + try + { + messagewriter req(request_type::readlink); + req.write_str(path); + auto reader = proxy.perform(std::move(req)); + int const result = reader.read_result(); + if (0 == result) + { + out = reader.read_str(); + } + return result; + } + catch(...) + { + return fallback.readlink(path, out); + } +} + +int filesystem::symlink(std::string const & target, std::string const & linkpath) +{ + try + { + messagewriter req(request_type::symlink); + req.write_str(target); + req.write_str(linkpath); + auto reader = proxy.perform(std::move(req)); + return reader.read_result(); + } + catch(...) + { + return fallback.symlink(target, linkpath); + } +} + +int filesystem::link(std::string const & old_path, std::string const & new_path) +{ + try + { + messagewriter req(request_type::link); + req.write_str(old_path); + req.write_str(new_path); + auto reader = proxy.perform(std::move(req)); + return reader.read_result(); + } + catch(...) + { + return fallback.link(old_path, new_path); + } +} + +int filesystem::rename(std::string const & old_path, std::string const & new_path, int flags) +{ + try + { + messagewriter req(request_type::rename); + req.write_str(old_path); + req.write_str(new_path); + req.write_rename_flags(flags); + auto reader = proxy.perform(std::move(req)); + return reader.read_result(); + } + catch(...) + { + return fallback.rename(old_path, new_path, flags); + } +} + +int filesystem::chmod(std::string const & path, mode_t mode) +{ + try + { + messagewriter req(request_type::chmod); + req.write_str(path); + req.write_mode(mode); + auto reader = proxy.perform(std::move(req)); + return reader.read_result(); + } + catch(...) + { + return fallback.chmod(path, mode); + } +} + +int filesystem::chown(std::string const & path, uid_t uid, gid_t gid) +{ + try + { + messagewriter req(request_type::chown); + req.write_str(path); + req.write_uid(uid); + req.write_gid(gid); + auto reader = proxy.perform(std::move(req)); + return reader.read_result(); + } + catch(...) + { + return fallback.chown(path, uid, gid); + } +} + +int filesystem::truncate(std::string const & path, uint64_t size, uint64_t handle) +{ + try + { + messagewriter req(request_type::truncate); + req.write_str(path); + req.write_u64(size); + req.write_u64(handle); + auto reader = proxy.perform(std::move(req)); + return reader.read_result(); + } + catch(...) + { + return fallback.truncate(path, size, handle); + } +} + +int filesystem::fsync(std::string const & path, bool is_datasync, uint64_t handle) +{ + try + { + messagewriter req(request_type::fsync); + req.write_str(path); + req.write_bool(is_datasync); + req.write_u64(handle); + auto reader = proxy.perform(std::move(req)); + return reader.read_result(); + } + catch(...) + { + return fallback.fsync(path, is_datasync, handle); + } +} + +int filesystem::utimens(std::string const &path, struct timespec const tv[2], uint64_t handle) +{ + try + { + messagewriter req(request_type::utimens); + req.write_str(path); + req.write_time(tv[0]); + req.write_time(tv[1]); + req.write_u64(handle); + auto reader = proxy.perform(std::move(req)); + return reader.read_result(); + } + catch(...) + { + return fallback.utimens(path, tv, handle); + } +} + + +int filesystem::open(std::string const & path, int flags, uint64_t & handle) +{ + try + { + messagewriter req(request_type::open); + req.write_str(path); + req.write_openflags(flags); + auto reader = proxy.perform(std::move(req)); + int result = reader.read_result(); + if (0 == result) + { + handle = reader.read_u64(); + } + return result; + } + catch(...) + { + return fallback.open(path, flags, handle); + } +} + +int filesystem::mknod(std::string const & path, mode_t mode, dev_t rdev) +{ + try + { + messagewriter req(request_type::mknod); + req.write_str(path); + req.write_mode(mode); + req.write_u64(rdev); + auto reader = proxy.perform(std::move(req)); + return reader.read_result(); + } + catch(...) + { + return fallback.mknod(path, mode, rdev); + } +} + +int filesystem::create(std::string const & path, mode_t mode, uint64_t & handle) +{ + try + { + messagewriter req(request_type::create); + req.write_str(path); + req.write_mode(mode); + auto reader = proxy.perform(std::move(req)); + int result = reader.read_result(); + if (0 == result) + { + handle = reader.read_u64(); + } + return result; + } + catch(...) + { + return fallback.create(path, mode, handle); + } +} + +int filesystem::release(std::string const & path, uint64_t handle) +{ + try + { + messagewriter req(request_type::release); + req.write_str(path); + req.write_u64(handle); + auto reader = proxy.perform(std::move(req)); + return reader.read_result(); + } + catch(...) + { + return fallback.release(path, handle); + } +} + +int filesystem::unlink(std::string const & path) +{ + try + { + messagewriter req(request_type::unlink); + req.write_str(path); + auto reader = proxy.perform(std::move(req)); + return reader.read_result(); + } + catch(...) + { + return fallback.unlink(path); + } +} + +int filesystem::read(std::string const & path, char * buffer, size_t buffer_size, uint64_t offset, uint64_t handle) +{ + try + { + messagewriter req(request_type::read); + req.write_str(path); + req.write_u32(buffer_size); + req.write_u64(offset); + req.write_u64(handle); + auto reader = proxy.perform(std::move(req)); + int result = reader.read_result(); + if (result > 0) + { + std::string data = reader.read_bytes(); + if (data.size() <= buffer_size) + { + memcpy(reinterpret_cast(buffer), reinterpret_cast(data.data()), data.size()); + } + else + { + throw std::runtime_error("invalid message"); + } + } + return result; + } + catch(...) + { + return fallback.read(path, buffer, buffer_size, offset, handle); + } +} + +int filesystem::write(std::string const & path, char const * buffer, size_t buffer_size, uint64_t offset, uint64_t handle) +{ + try + { + messagewriter req(request_type::write); + req.write_str(path); + req.write_data(buffer, buffer_size); + req.write_u64(offset); + req.write_u64(handle); + auto reader = proxy.perform(std::move(req)); + return reader.read_result(); + } + catch(...) + { + return fallback.write(path, buffer, buffer_size, offset, handle); + } +} + +int filesystem::mkdir(std::string const & path, mode_t mode) +{ + try + { + messagewriter req(request_type::mkdir); + req.write_str(path); + req.write_mode(mode); + auto reader = proxy.perform(std::move(req)); + return reader.read_result(); + } + catch(...) + { + return fallback.mkdir(path, mode); + } +} + +int filesystem::readdir(std::string const & path, std::vector & entries) +{ + try + { + messagewriter req(request_type::readdir); + req.write_str(path); + auto resp = proxy.perform(std::move(req)); + int result = resp.read_result(); + if (0 == result) + { + resp.read_strings(entries); + } + return result; + } + catch(...) + { + return fallback.readdir(path, entries); + } +} + +int filesystem::rmdir(std::string const & path) +{ + try + { + messagewriter req(request_type::rmdir); + req.write_str(path); + auto reader = proxy.perform(std::move(req)); + return reader.read_result(); + } + catch(...) + { + return fallback.rmdir(path); + } +} + +int filesystem::statfs(std::string const & path, struct statvfs * statistics) +{ + try + { + messagewriter req(request_type::statfs); + req.write_str(path); + auto reader = proxy.perform(std::move(req)); + int result = reader.read_result(); + if (0 == result) + { + reader.read_statistics(statistics); + } + return result; + } + catch(...) + { + return fallback.statfs(path, statistics); + } +} + +// get credentials is handled internally +std::string filesystem::get_credentials() +{ + throw std::runtime_error("not implemented"); +} + + +} diff --git a/src/webfuse/filesystem.hpp b/src/webfuse/filesystem.hpp new file mode 100644 index 0000000..e57c7a2 --- /dev/null +++ b/src/webfuse/filesystem.hpp @@ -0,0 +1,59 @@ +#ifndef WEBFUSE_FILESYSTEM_HPP +#define WEBFUSE_FILESYSTEM_HPP + +#include "webfuse/filesystem/filesystem_i.hpp" +#include "webfuse/filesystem/empty_filesystem.hpp" +#include "webfuse/ws/server.hpp" + +namespace webfuse +{ + +class filesystem: public filesystem_i +{ + filesystem(filesystem const &) = delete; + filesystem& operator=(filesystem const &) = delete; + filesystem(filesystem &&) = delete; + filesystem& operator=(filesystem &&) = delete; +public: + explicit filesystem(ws_server& server); + ~filesystem() override; + + int access(std::string const & path, int mode) override; + int getattr(std::string const & path, struct stat * attr) override; + + int readlink(std::string const & path, std::string & out) override; + int symlink(std::string const & target, std::string const & linkpath) override; + int link(std::string const & old_path, std::string const & new_path) override; + + int rename(std::string const & old_path, std::string const & new_path, int flags) override; + int chmod(std::string const & path, mode_t mode) override; + int chown(std::string const & path, uid_t uid, gid_t gid) override; + int truncate(std::string const & path, uint64_t size, uint64_t handle) override; + int fsync(std::string const & path, bool is_datasync, uint64_t handle) override; + int utimens(std::string const &path, struct timespec const tv[2], uint64_t handle) override; + + int open(std::string const & path, int flags, uint64_t & handle) override; + int mknod(std::string const & path, mode_t mode, dev_t rdev) override; + int create(std::string const & path, mode_t mode, uint64_t & handle) override; + int release(std::string const & path, uint64_t handle) override; + int unlink(std::string const & path) override; + + int read(std::string const & path, char * buffer, size_t buffer_size, uint64_t offset, uint64_t handle) override; + int write(std::string const & path, char const * buffer, size_t buffer_size, uint64_t offset, uint64_t handle) override; + + int mkdir(std::string const & path, mode_t mode) override; + int readdir(std::string const & path, std::vector & entries) override; + int rmdir(std::string const & path) override; + + int statfs(std::string const & path, struct statvfs * statistics) override; + + std::string get_credentials() override; + +private: + ws_server &proxy; + empty_filesystem fallback; +}; + +} + +#endif diff --git a/src/webfuse/filesystem/accessmode.cpp b/src/webfuse/filesystem/accessmode.cpp new file mode 100644 index 0000000..50abab4 --- /dev/null +++ b/src/webfuse/filesystem/accessmode.cpp @@ -0,0 +1,41 @@ +#include "webfuse/filesystem/accessmode.hpp" +#include + +namespace webfuse +{ + +access_mode::access_mode(int8_t value) +: value_(value) +{ + +} + +access_mode::operator int8_t() const +{ + return value_; +} + +access_mode access_mode::from_int(int value) +{ + int8_t result = 0; + + if (0 != (value & R_OK)) { result |= r_ok; } + if (0 != (value & W_OK)) { result |= w_ok; } + if (0 != (value & X_OK)) { result |= x_ok; } + + return access_mode(result); +} + +int access_mode::to_int() const +{ + int result = 0; + + if (0 != (value_ & r_ok)) { result |= R_OK; } + if (0 != (value_ & w_ok)) { result |= W_OK; } + if (0 != (value_ & x_ok)) { result |= X_OK; } + + return result; +} + + +} diff --git a/src/webfuse/filesystem/accessmode.hpp b/src/webfuse/filesystem/accessmode.hpp new file mode 100644 index 0000000..57b8139 --- /dev/null +++ b/src/webfuse/filesystem/accessmode.hpp @@ -0,0 +1,28 @@ +#ifndef WEBFUSE_ACCESSMODE_HPP +#define WEBFUSE_ACCESSMODE_HPP + +#include + +namespace webfuse +{ + +class access_mode +{ +public: + static constexpr int8_t const f_ok = 0; // F_OK + static constexpr int8_t const r_ok = 4; // R_OK + static constexpr int8_t const w_ok = 2; // W_OK + static constexpr int8_t const x_ok = 1; // X_OK + + access_mode(int8_t value = f_ok); + operator int8_t() const; + + static access_mode from_int(int value); + int to_int() const; +private: + int8_t value_; +}; + +} + +#endif diff --git a/src/webfuse/filesystem/empty_filesystem.cpp b/src/webfuse/filesystem/empty_filesystem.cpp new file mode 100644 index 0000000..b5ce3f5 --- /dev/null +++ b/src/webfuse/filesystem/empty_filesystem.cpp @@ -0,0 +1,140 @@ +#include "webfuse/filesystem/empty_filesystem.hpp" +#include + +namespace webfuse +{ + +int empty_filesystem::access(std::string const & path, int mode) +{ + if (path == "/") + { + return 0; + } + + return -ENOENT; +} + +int empty_filesystem::getattr(std::string const & path, struct stat * attr) +{ + if (path == "/") + { + attr->st_ino = 1; + attr->st_nlink = 1; + attr->st_mode = S_IFDIR | 0555; // NOLINT(readability-magic-numbers) + return 0; + } + + return -ENOENT; +} + +int empty_filesystem::readlink(std::string const & path, std::string & out) +{ + return -ENOENT; +} + +int empty_filesystem::symlink(std::string const & target, std::string const & linkpath) +{ + return -ENOENT; +} + +int empty_filesystem::link(std::string const & old_path, std::string const & new_path) +{ + return -ENOENT; +} + +int empty_filesystem::rename(std::string const & old_path, std::string const & new_path, int flags) +{ + return -ENOENT; +} + +int empty_filesystem::chmod(std::string const & path, mode_t mode) +{ + return -EPERM; +} + +int empty_filesystem::chown(std::string const & path, uid_t uid, gid_t gid) +{ + return -EPERM; +} + +int empty_filesystem::truncate(std::string const & path, uint64_t size, uint64_t handle) +{ + return -EPERM; +} + +int empty_filesystem::fsync(std::string const & path, bool is_datasync, uint64_t handle) +{ + return 0; +} + +int empty_filesystem::utimens(std::string const &path, struct timespec const tv[2], uint64_t handle) +{ + return -ENOSYS; +} + +int empty_filesystem::open(std::string const & path, int flags, uint64_t & handle) +{ + return -ENOENT; +} + +int empty_filesystem::mknod(std::string const & path, mode_t mode, dev_t rdev) +{ + return -EPERM; + +} +int empty_filesystem::create(std::string const & path, mode_t mode, uint64_t & handle) +{ + return -EPERM; +} + +int empty_filesystem::release(std::string const & path, uint64_t handle) +{ + return 0; +} + +int empty_filesystem::unlink(std::string const & path) +{ + return -EPERM; +} + +int empty_filesystem::read(std::string const & path, char * buffer, size_t buffer_size, uint64_t offset, uint64_t handle) +{ + return -EBADF; +} +int empty_filesystem::write(std::string const & path, char const * buffer, size_t buffer_size, uint64_t offset, uint64_t handle) +{ + return -EBADF; +} + +int empty_filesystem::mkdir(std::string const & path, mode_t mode) +{ + return -EPERM; +} + +int empty_filesystem::readdir(std::string const & path, std::vector & entries) +{ + if (path == "/") + { + return 0; + } + + return -ENOENT; +} + +int empty_filesystem::rmdir(std::string const & path) +{ + return -EPERM; +} + +int empty_filesystem::statfs(std::string const & path, struct statvfs * statistics) +{ + return -ENOSYS; +} + +std::string empty_filesystem::get_credentials() +{ + return ""; +} + + +} diff --git a/src/webfuse/filesystem/empty_filesystem.hpp b/src/webfuse/filesystem/empty_filesystem.hpp new file mode 100644 index 0000000..d4cca3a --- /dev/null +++ b/src/webfuse/filesystem/empty_filesystem.hpp @@ -0,0 +1,48 @@ +#ifndef WEBFUSE_EMPTYFILESYSTEM_HPP +#define WEBFUSE_EMPTYFILESYSTEM_HPP + +#include "webfuse/filesystem/filesystem_i.hpp" + +namespace webfuse +{ + +class empty_filesystem: public filesystem_i +{ +public: + ~empty_filesystem() override = default; + + int access(std::string const & path, int mode) override; + int getattr(std::string const & path, struct stat * attr) override; + + int readlink(std::string const & path, std::string & out) override; + int symlink(std::string const & target, std::string const & linkpath) override; + int link(std::string const & old_path, std::string const & new_path) override; + + int rename(std::string const & old_path, std::string const & new_path, int flags) override; + int chmod(std::string const & path, mode_t mode) override; + int chown(std::string const & path, uid_t uid, gid_t gid) override; + int truncate(std::string const & path, uint64_t size, uint64_t handle) override; + int fsync(std::string const & path, bool is_datasync, uint64_t handle) override; + int utimens(std::string const &path, struct timespec const tv[2], uint64_t handle) override; + + int open(std::string const & path, int flags, uint64_t & handle) override; + int mknod(std::string const & path, mode_t mode, dev_t rdev) override; + int create(std::string const & path, mode_t mode, uint64_t & handle) override; + int release(std::string const & path, uint64_t handle) override; + int unlink(std::string const & path) override; + + int read(std::string const & path, char * buffer, size_t buffer_size, uint64_t offset, uint64_t handle) override; + int write(std::string const & path, char const * buffer, size_t buffer_size, uint64_t offset, uint64_t handle) override; + + int mkdir(std::string const & path, mode_t mode) override; + int readdir(std::string const & path, std::vector & entries) override; + int rmdir(std::string const & path) override; + + int statfs(std::string const & path, struct statvfs * statistics) override; + + std::string get_credentials() override; +}; + +} + +#endif diff --git a/src/webfuse/filesystem/filemode.cpp b/src/webfuse/filesystem/filemode.cpp new file mode 100644 index 0000000..0b962eb --- /dev/null +++ b/src/webfuse/filesystem/filemode.cpp @@ -0,0 +1,50 @@ +#include "webfuse/filesystem/filemode.hpp" +#include + +namespace webfuse +{ + +filemode::filemode(uint32_t value) +: value_(value) +{ + +} + +filemode::operator uint32_t() const +{ + return value_; +} + + +filemode filemode::from_mode(mode_t value) +{ + uint32_t result = value & 07777; // NOLINT(readability-magic-numbers) + + if (S_ISREG(value) ) { result |= filemode::reg; } + if (S_ISDIR(value) ) { result |= filemode::dir; } + if (S_ISCHR(value) ) { result |= filemode::chr; } + if (S_ISBLK(value) ) { result |= filemode::blk; } + if (S_ISFIFO(value)) { result |= filemode::fifo; } + if (S_ISLNK(value) ) { result |= filemode::lnk; } + if (S_ISSOCK(value)) { result |= filemode::sock; } + + return filemode(result); +} + +mode_t filemode::to_mode() const +{ + mode_t result = value_ & 07777; // NOLINT(readability-magic-numbers) + + if (is_reg() ) { result |= S_IFREG; } + if (is_dir() ) { result |= S_IFDIR; } + if (is_chr() ) { result |= S_IFCHR; } + if (is_blk() ) { result |= S_IFBLK; } + if (is_fifo()) { result |= S_IFIFO; } + if (is_lnk() ) { result |= S_IFLNK; } + if (is_sock()) { result |= S_IFSOCK; } + + return result; +} + + +} diff --git a/src/webfuse/filesystem/filemode.hpp b/src/webfuse/filesystem/filemode.hpp new file mode 100644 index 0000000..64af918 --- /dev/null +++ b/src/webfuse/filesystem/filemode.hpp @@ -0,0 +1,75 @@ +#ifndef WEBFUSE_FILEMODE_HPP +#define WEBFUSE_FILEMODE_HPP + +#include +#include + +namespace webfuse +{ + +class filemode +{ +public: + static constexpr uint32_t const protection_mask = 0000777; + static constexpr uint32_t const sticky_mask = 0007000; + static constexpr uint32_t const filetype_mask = 0170000; + + static constexpr uint32_t const suid = 04000; // S_ISUID + static constexpr uint32_t const sgid = 02000; // S_ISGID + static constexpr uint32_t const svtx = 01000; // S_ISVTX + + static constexpr uint32_t const reg = 0100000; // S_IFREG + static constexpr uint32_t const dir = 0040000; // S_IFDIR + static constexpr uint32_t const chr = 0020000; // S_IFCHR + static constexpr uint32_t const blk = 0060000; // S_IFBLK + static constexpr uint32_t const fifo = 0010000; // S_IFIFO + static constexpr uint32_t const lnk = 0120000; // S_IFLNK + static constexpr uint32_t const sock = 0140000; // S_IFSOCK + + explicit filemode(uint32_t value = 0); + operator uint32_t() const; + static filemode from_mode(mode_t value); + mode_t to_mode() const; + + inline bool is_reg() const + { + return (filemode::reg == (value_ & filemode::filetype_mask)); + } + + inline bool is_dir() const + { + return (filemode::dir == (value_ & filemode::filetype_mask)); + } + + inline bool is_chr() const + { + return (filemode::chr == (value_ & filemode::filetype_mask)); + } + + inline bool is_blk() const + { + return (filemode::blk == (value_ & filemode::filetype_mask)); + } + + inline bool is_fifo() const + { + return (filemode::fifo == (value_ & filemode::filetype_mask)); + } + + inline bool is_lnk() const + { + return (filemode::lnk == (value_ & filemode::filetype_mask)); + } + + inline bool is_sock() const + { + return (filemode::sock == (value_ & filemode::filetype_mask)); + } + +private: + uint32_t value_; +}; + +} + +#endif diff --git a/src/webfuse/filesystem/filesystem_i.hpp b/src/webfuse/filesystem/filesystem_i.hpp new file mode 100644 index 0000000..67abb64 --- /dev/null +++ b/src/webfuse/filesystem/filesystem_i.hpp @@ -0,0 +1,57 @@ +#ifndef WEBFUSE_FILESYSTEM_I_HPP +#define WEBFUSE_FILESYSTEM_I_HPP + +#include +#include +#include +#include + +#include +#include +#include + +namespace webfuse +{ + +constexpr uint64_t const invalid_handle = static_cast(-1); + +class filesystem_i +{ +public: + virtual ~filesystem_i() = default; + + virtual int access(std::string const & path, int mode) = 0; + virtual int getattr(std::string const & path, struct stat * attr) = 0; + + virtual int readlink(std::string const & path, std::string & out) = 0; + virtual int symlink(std::string const & target, std::string const & linkpath) = 0; + virtual int link(std::string const & old_path, std::string const & new_path) = 0; + + virtual int rename(std::string const & old_path, std::string const & new_path, int flags) = 0; + virtual int chmod(std::string const & path, mode_t mode) = 0; + virtual int chown(std::string const & path, uid_t uid, gid_t gid) = 0; + virtual int truncate(std::string const & path, uint64_t size, uint64_t handle) = 0; + virtual int fsync(std::string const & path, bool is_datasync, uint64_t handle) = 0; + virtual int utimens(std::string const &path, struct timespec const tv[2], uint64_t handle) = 0; + + virtual int open(std::string const & path, int flags, uint64_t & handle) = 0; + virtual int mknod(std::string const & path, mode_t mode, dev_t rdev) = 0; + virtual int create(std::string const & path, mode_t mode, uint64_t & handle) = 0; + virtual int release(std::string const & path, uint64_t handle) = 0; + virtual int unlink(std::string const & path) = 0; + + virtual int read(std::string const & path, char * buffer, size_t buffer_size, uint64_t offset, uint64_t handle) = 0; + virtual int write(std::string const & path, char const * buffer, size_t buffer_size, uint64_t offset, uint64_t handle) = 0; + + virtual int mkdir(std::string const & path, mode_t mode) = 0; + virtual int readdir(std::string const & path, std::vector & entries) = 0; + virtual int rmdir(std::string const & path) = 0; + + virtual int statfs(std::string const & path, struct statvfs * statistics) = 0; + + virtual std::string get_credentials() = 0; +}; + +} + +#endif diff --git a/src/webfuse/filesystem/filesystem_statistics.cpp b/src/webfuse/filesystem/filesystem_statistics.cpp new file mode 100644 index 0000000..f1bd09b --- /dev/null +++ b/src/webfuse/filesystem/filesystem_statistics.cpp @@ -0,0 +1,43 @@ +#include "webfuse/filesystem/filesystem_statistics.hpp" + +namespace webfuse +{ + +filesystem_statistics::filesystem_statistics() +: bsize(0) +, frsize(0) +, blocks(0) +, bfree(0) +, bavail(0) +, files(0) +, ffree(0) +, f_namemax(0) +{ + +} + +filesystem_statistics::filesystem_statistics(struct statvfs const & other) +{ + bsize = other.f_bsize; + frsize = other.f_frsize; + blocks = other.f_blocks; + bfree = other.f_bfree; + bavail = other.f_bavail; + files = other.f_files; + ffree = other.f_ffree; + f_namemax = other.f_namemax; +} + +void filesystem_statistics::copy_to(struct statvfs & other) const +{ + other.f_bsize = bsize; + other.f_frsize = frsize; + other.f_blocks = blocks; + other.f_bfree = bfree; + other.f_bavail = bavail; + other.f_files = files; + other.f_ffree = ffree; + other.f_namemax = f_namemax; +} + +} diff --git a/src/webfuse/filesystem/filesystem_statistics.hpp b/src/webfuse/filesystem/filesystem_statistics.hpp new file mode 100644 index 0000000..2534a09 --- /dev/null +++ b/src/webfuse/filesystem/filesystem_statistics.hpp @@ -0,0 +1,31 @@ +#ifndef WEBFUSE_FILESYSTEMSTATISTICS_HPP +#define WEBFUSE_FILESYSTEMSTATISTICS_HPP + +#include +#include + +namespace webfuse +{ + +class filesystem_statistics +{ +public: + filesystem_statistics(); + explicit filesystem_statistics(struct statvfs const & other); + void copy_to(struct statvfs & other) const; + + uint64_t bsize; + uint64_t frsize; + uint64_t blocks; + uint64_t bfree; + uint64_t bavail; + uint64_t files; + uint64_t ffree; + uint64_t f_namemax; +}; + + + +} + +#endif diff --git a/src/webfuse/filesystem/openflags.cpp b/src/webfuse/filesystem/openflags.cpp new file mode 100644 index 0000000..4700e8d --- /dev/null +++ b/src/webfuse/filesystem/openflags.cpp @@ -0,0 +1,79 @@ +#include "webfuse/filesystem/openflags.hpp" +#include + +namespace webfuse +{ + +openflags::openflags(int32_t value) +: value_(value) +{ + +} + +openflags::operator int32_t() const +{ + return value_; +} + +openflags openflags::from_int(int value) +{ + int32_t result = 0; + + if (O_RDONLY == (value & O_RDONLY )) { result |= openflags::rdonly; } + if (O_WRONLY == (value & O_WRONLY )) { result |= openflags::wronly; } + if (O_RDWR == (value & O_RDWR )) { result |= openflags::rdwr; } + + if (O_APPEND == (value & O_APPEND )) { result |= openflags::append; } + if (O_ASYNC == (value & O_ASYNC )) { result |= openflags::async; } + if (O_CLOEXEC == (value & O_CLOEXEC )) { result |= openflags::cloexec; } + if (O_CREAT == (value & O_CREAT )) { result |= openflags::creat; } + if (O_DIRECT == (value & O_DIRECT )) { result |= openflags::direct; } + if (O_DIRECTORY == (value & O_DIRECTORY)) { result |= openflags::directory; } + if (O_DSYNC == (value & O_DSYNC )) { result |= openflags::dsync; } + if (O_EXCL == (value & O_EXCL )) { result |= openflags::excl; } + if (O_LARGEFILE == (value & O_LARGEFILE)) { result |= openflags::largefile; } + if (O_NOATIME == (value & O_NOATIME )) { result |= openflags::noatime; } + if (O_NOCTTY == (value & O_NOCTTY )) { result |= openflags::noctty; } + if (O_NOFOLLOW == (value & O_NOFOLLOW )) { result |= openflags::nofollow; } + if (O_NONBLOCK == (value & O_NONBLOCK )) { result |= openflags::nonblock; } + if (O_NDELAY == (value & O_NDELAY )) { result |= openflags::ndelay; } + if (O_PATH == (value & O_PATH )) { result |= openflags::path; } + if (O_SYNC == (value & O_SYNC )) { result |= openflags::sync; } + if (O_TMPFILE == (value & O_TMPFILE )) { result |= openflags::tmpfile; } + if (O_TRUNC == (value & O_TRUNC )) { result |= openflags::trunc; } + + return openflags(result); +} + +int openflags::to_int() const +{ + int result = 0; + + if (openflags::rdonly == (value_ & openflags::rdonly )) { result |= O_RDONLY; } + if (openflags::wronly == (value_ & openflags::wronly )) { result |= O_WRONLY; } + if (openflags::rdwr == (value_ & openflags::rdwr )) { result |= O_RDWR; } + + if (openflags::append == (value_ & openflags::append )) { result |= O_APPEND; } + if (openflags::async == (value_ & openflags::async )) { result |= O_ASYNC; } + if (openflags::cloexec == (value_ & openflags::cloexec )) { result |= O_CLOEXEC; } + if (openflags::creat == (value_ & openflags::creat )) { result |= O_CREAT; } + if (openflags::direct == (value_ & openflags::direct )) { result |= O_DIRECT; } + if (openflags::directory == (value_ & openflags::directory)) { result |= O_DIRECTORY; } + if (openflags::dsync == (value_ & openflags::dsync )) { result |= O_DSYNC; } + if (openflags::excl == (value_ & openflags::excl )) { result |= O_EXCL; } + if (openflags::largefile == (value_ & openflags::largefile)) { result |= O_LARGEFILE; } + if (openflags::noatime == (value_ & openflags::noatime )) { result |= O_NOATIME; } + if (openflags::noctty == (value_ & openflags::noctty )) { result |= O_NOCTTY; } + if (openflags::nofollow == (value_ & openflags::nofollow )) { result |= O_NOFOLLOW; } + if (openflags::nonblock == (value_ & openflags::nonblock )) { result |= O_NONBLOCK; } + if (openflags::ndelay == (value_ & openflags::ndelay )) { result |= O_NDELAY; } + if (openflags::path == (value_ & openflags::path )) { result |= O_PATH; } + if (openflags::sync == (value_ & openflags::sync )) { result |= O_SYNC; } + if (openflags::tmpfile == (value_ & openflags::tmpfile )) { result |= O_TMPFILE; } + if (openflags::trunc == (value_ & openflags::trunc )) { result |= O_TRUNC; } + + return result; +} + + +} diff --git a/src/webfuse/filesystem/openflags.hpp b/src/webfuse/filesystem/openflags.hpp new file mode 100644 index 0000000..8405332 --- /dev/null +++ b/src/webfuse/filesystem/openflags.hpp @@ -0,0 +1,47 @@ +#ifndef WEBFUSE_OPENFLAGS_HPP +#define WEBFUSE_OPENFLAGS_HPP + +#include + +namespace webfuse +{ + +class openflags +{ +public: + static constexpr int32_t const accessmode_mask = 03; // O_ACCMODE + static constexpr int32_t const rdonly = 0; // O_RDONLY + static constexpr int32_t const wronly = 01; // O_WRONLY + static constexpr int32_t const rdwr = 02; // O_RDWR + + static constexpr int32_t const append = 02000; // O_APPEND + static constexpr int32_t const async = 020000; // O_ASYNC + static constexpr int32_t const cloexec = 02000000; // O_CLOEXEC + static constexpr int32_t const creat = 0100; // O_CREAT + static constexpr int32_t const direct = 040000; // O_DIRECT + static constexpr int32_t const directory = 0200000; // O_DIRECTORY + static constexpr int32_t const dsync = 010000; // O_DSYNC + static constexpr int32_t const excl = 0200; // O_EXCL + static constexpr int32_t const largefile = 0100000; // O_LARGEFILE + static constexpr int32_t const noatime = 01000000; // O_NOATIME + static constexpr int32_t const noctty = 0400; // O_NOCTTY + static constexpr int32_t const nofollow = 0400000; // O_NOFOLLOW + static constexpr int32_t const nonblock = 04000; // O_NONBLOCK + static constexpr int32_t const ndelay = 04000; // O_NDELAY + static constexpr int32_t const path = 010000000; // O_PATH + static constexpr int32_t const sync = 04010000; // O_SYNC + static constexpr int32_t const tmpfile = 020200000; // O_TMPFILE + static constexpr int32_t const trunc = 01000; // O_TRUNC + + explicit openflags(int32_t value = 0); + operator int32_t() const; + + static openflags from_int(int value); + int to_int() const; +private: + int32_t value_; +}; + +} + +#endif diff --git a/src/webfuse/filesystem/status.cpp b/src/webfuse/filesystem/status.cpp new file mode 100644 index 0000000..4ba6e50 --- /dev/null +++ b/src/webfuse/filesystem/status.cpp @@ -0,0 +1,122 @@ +#include "webfuse/filesystem/status.hpp" +#include + +namespace webfuse +{ + +status::status(int32_t value) +: value_(value) +{ + +} + +status::operator int32_t() const +{ + return value_; +} + +status status::from_fusestatus(int value) +{ + if (value >= 0) + { + return static_cast(value); + } + + switch(value) + { + case -E2BIG: return status::bad_e2big; + case -EACCES: return status::bad_eacces; + case -EAGAIN: return status::bad_eagain; + case -EBADF: return status::bad_ebadf; + case -EBUSY: return status::bad_ebusy; + case -EDESTADDRREQ: return status::bad_edestaddrreq; + case -EDQUOT: return status::bad_edquot; + case -EEXIST: return status::bad_eexist; + case -EFAULT: return status::bad_efault; + case -EFBIG: return status::bad_efbig; + case -EINTR: return status::bad_eintr; + case -EINVAL: return status::bad_einval; + case -EIO: return status::bad_eio; + case -EISDIR: return status::bad_eisdir; + case -ELOOP: return status::bad_eloop; + case -EMFILE: return status::bad_emfile; + case -EMLINK: return status::bad_emlink; + case -ENAMETOOLONG: return status::bad_enametoolong; + case -ENFILE: return status::bad_enfile; + case -ENODATA: return status::bad_enodata; + case -ENODEV: return status::bad_enodev; + case -ENOENT: return status::bad_enoent; + case -ENOMEM: return status::bad_enomem; + case -ENOSPC: return status::bad_enospc; + case -ENOSYS: return status::bad_enosys; + case -ENOTDIR: return status::bad_enotdir; + case -ENOTEMPTY: return status::bad_enotempty; + case -ENOTSUP: return status::bad_enotsup; + case -ENXIO: return status::bad_enxio; + case -EOVERFLOW: return status::bad_eoverflow; + case -EPERM: return status ::bad_eperm; + case -EPIPE: return status::bad_epipe; + case -ERANGE: return status::bad_erange; + case -EROFS: return status::bad_erofs; + case -ETXTBSY: return status::bad_etxtbsy; + case -EXDEV: return status::bad_exdev; + default: return static_cast(value); + } +} + +int status::to_fusestatus() const +{ + if (value_ >= 0) + { + return static_cast(value_); + } + + switch(value_) + { + case status::bad_e2big: return -E2BIG; + case status::bad_eacces: return -EACCES; + case status::bad_eagain: return -EAGAIN; + case status::bad_ebadf: return -EBADF; + case status::bad_ebusy: return -EBUSY; + case status::bad_edestaddrreq: return -EDESTADDRREQ; + case status::bad_edquot: return -EDQUOT; + case status::bad_eexist: return -EEXIST; + case status::bad_efault: return -EFAULT; + case status::bad_efbig: return -EFBIG; + case status::bad_eintr: return -EINTR; + case status::bad_einval: return -EINVAL; + case status::bad_eio: return -EIO; + case status::bad_eisdir: return -EISDIR; + case status::bad_eloop: return -ELOOP; + case status::bad_emfile: return -EMFILE; + case status::bad_emlink: return -EMLINK; + case status::bad_enametoolong: return -ENAMETOOLONG; + case status::bad_enfile: return -ENFILE; + case status::bad_enodata: return -ENODATA; + case status::bad_enodev: return -ENODEV; + case status::bad_enoent: return -ENOENT; + case status::bad_enomem: return -ENOMEM; + case status::bad_enospc: return -ENOSPC; + case status::bad_enosys: return -ENOSYS; + case status::bad_enotdir: return -ENOTDIR; + case status::bad_enotempty: return -ENOTEMPTY; + case status::bad_enotsup: return -ENOTSUP; + case status::bad_enxio: return -ENXIO; + case status::bad_eoverflow: return -EOVERFLOW; + case status::bad_eperm: return -EPERM; + case status::bad_epipe: return -EPIPE; + case status::bad_erange: return -ERANGE; + case status::bad_erofs: return -EROFS; + case status::bad_etxtbsy: return -ETXTBSY; + case status::bad_exdev: return -EXDEV; + default: return static_cast(value_); + } +} + +bool status::is_good() const +{ + return (value_ == status::good); +} + + +} diff --git a/src/webfuse/filesystem/status.hpp b/src/webfuse/filesystem/status.hpp new file mode 100644 index 0000000..e946221 --- /dev/null +++ b/src/webfuse/filesystem/status.hpp @@ -0,0 +1,64 @@ +#ifndef WEBFUSE_STATUS_HPP +#define WEBFUSE_STATUS_HPP + +#include + +namespace webfuse +{ + +class status +{ +public: + static constexpr int32_t const good = 0; + static constexpr int32_t const bad_e2big = -7; // -E2BIG + static constexpr int32_t const bad_eacces = -13; // -EACCES + static constexpr int32_t const bad_eagain = -11; // -EAGAIN + static constexpr int32_t const bad_ebadf = -9; // -EBADF + static constexpr int32_t const bad_ebusy = -16; // -EBUSY + static constexpr int32_t const bad_edestaddrreq = -89; // -EDESTADDRREQ + static constexpr int32_t const bad_edquot = -122; // -EDQUOT + static constexpr int32_t const bad_eexist = -17; // -EEXIST + static constexpr int32_t const bad_efault = -14; // -EFAULT + static constexpr int32_t const bad_efbig = -27; // -EFBIG + static constexpr int32_t const bad_eintr = -4; // -EINTR + static constexpr int32_t const bad_einval = -22; // -EINVAL + static constexpr int32_t const bad_eio = -5; // -EIO + static constexpr int32_t const bad_eisdir = -21; // -EISDIR + static constexpr int32_t const bad_eloop = -40; // -ELOOP + static constexpr int32_t const bad_emfile = -24; // -EMFILE + static constexpr int32_t const bad_emlink = -31; // -EMLINK + static constexpr int32_t const bad_enametoolong = -36; // -ENAMETOOLONG + static constexpr int32_t const bad_enfile = -23; // -ENFILE + static constexpr int32_t const bad_enodata = -61; // -ENODATA + static constexpr int32_t const bad_enodev = -19; // -ENODEV + static constexpr int32_t const bad_enoent = -2; // -ENOENT + static constexpr int32_t const bad_enomem = -12; // -ENOMEM + static constexpr int32_t const bad_enospc = -28; // -ENOSPC + static constexpr int32_t const bad_enosys = -38; // -ENOSYS + static constexpr int32_t const bad_enotdir = -20; // -ENOTDIR + static constexpr int32_t const bad_enotempty = -39; // -ENOTEMPTY + static constexpr int32_t const bad_enotsup = -95; // -ENOTSUP + static constexpr int32_t const bad_enxio = -6; // -ENXIO + static constexpr int32_t const bad_eoverflow = -75; // -EOVERFLOW + static constexpr int32_t const bad_eperm = -1; // -EPERM + static constexpr int32_t const bad_epipe = -32; // -EPIPE + static constexpr int32_t const bad_erange = -34; // -ERANGE + static constexpr int32_t const bad_erofs = -30; // -EROFS + static constexpr int32_t const bad_etxtbsy = -26; // -ETXTBSY + static constexpr int32_t const bad_exdev = -18; // -EXDEV + static constexpr int32_t const bad_ewouldblock = -11; // -EWOULDBLOCK + + status(int32_t value = status::good); + operator int32_t() const; + + static status from_fusestatus(int value); + int to_fusestatus() const; + + bool is_good() const; +private: + int32_t value_; +}; + +} + +#endif diff --git a/src/webfuse/fuse.cpp b/src/webfuse/fuse.cpp new file mode 100644 index 0000000..89c49ea --- /dev/null +++ b/src/webfuse/fuse.cpp @@ -0,0 +1,304 @@ +#define FUSE_USE_VERSION 31 + +#include "webfuse/fuse.hpp" +#include +#include +#include + +extern "C" +{ + +static webfuse::filesystem_i * fs_get_filesystem() +{ + struct fuse_context * context = fuse_get_context(); + void * private_data = context->private_data; + return reinterpret_cast(private_data); +} + +static uint64_t fs_get_handle(fuse_file_info * info) +{ + return (nullptr != info) ? info->fh : ((uint64_t) -1); +} + +static void * fs_init(fuse_conn_info * connection, fuse_config * config) +{ + (void) connection; + config->use_ino = 1; + config->entry_timeout = 0; + config->attr_timeout = 0; + config->negative_timeout = 0; + + struct fuse_context * context = fuse_get_context(); + return context->private_data; +} + + +static int fs_access(char const * path, int mode) +{ + auto * const fs = fs_get_filesystem(); + return fs->access(path, mode); +} + +static int fs_getattr(char const * path, struct stat * buffer, fuse_file_info * info) +{ + (void) info; + + auto * const fs = fs_get_filesystem(); + return fs->getattr(path, buffer); +} + +static int fs_readlink(char const * path, char * buffer, size_t buffer_size) +{ + auto * const fs = fs_get_filesystem(); + + std::string out; + auto result = fs->readlink(path, out); + if (0 == result) + { + snprintf(buffer, buffer_size, "%s", out.c_str()); + } + + return result; +} + +static int fs_symlink(char const * target, char const * linkpath) +{ + auto * const fs = fs_get_filesystem(); + return fs->symlink(target, linkpath); +} + +static int fs_link(char const * old_path, char const * new_path) +{ + auto * const fs = fs_get_filesystem(); + return fs->link(old_path, new_path); +} + +static int fs_rename(char const * from, char const * to, unsigned int flags) +{ + auto * const fs = fs_get_filesystem(); + return fs->rename(from, to, flags); +} + +static int fs_chmod(char const * path, mode_t mode, fuse_file_info * info) +{ + (void) info; + + auto * const fs = fs_get_filesystem(); + return fs->chmod(path, mode); +} + +static int fs_chown(char const * path, uid_t uid, gid_t gid, fuse_file_info * info) +{ + (void) info; + + auto * const fs = fs_get_filesystem(); + return fs->chown(path, uid, gid); +} + +static int fs_truncate(char const * path, off_t size, fuse_file_info * info) +{ + auto * const fs = fs_get_filesystem(); + auto const handle = fs_get_handle(info); + + return fs->truncate(path, size, handle); +} + +static int fs_fsync(char const * path, int isdatasync, fuse_file_info * info) +{ + auto * const fs = fs_get_filesystem(); + bool const is_datasync = (isdatasync != 0); + auto const handle = fs_get_handle(info); + + return fs->fsync(path, is_datasync, handle); +} + +static int fs_open(char const * path, fuse_file_info * info) +{ + auto * const fs = fs_get_filesystem(); + auto const flags = info->flags; + + return fs->open(path, flags, info->fh); +} + +static int fs_mknod(char const * path, mode_t mode, dev_t raw_rdev) +{ + auto * const fs = fs_get_filesystem(); + auto const rdev = static_cast(raw_rdev); + + return fs->mknod(path, mode, rdev); +} + +static int fs_create(char const * path, mode_t mode, fuse_file_info * info) +{ + auto * const fs = fs_get_filesystem(); + + return fs->create(path, mode, info->fh); +} + +static int fs_release(char const * path, fuse_file_info * info) +{ + auto * const fs = fs_get_filesystem(); + auto const handle = fs_get_handle(info); + + return fs->release(path, handle); +} + +static int fs_unlink(char const * path) +{ + auto * const fs = fs_get_filesystem(); + return fs->unlink(path); +} + +static int fs_read(char const * path, char * buffer, + size_t buffer_size, off_t raw_offset, + fuse_file_info * info) +{ + auto * const fs = fs_get_filesystem(); + auto const offset = static_cast(raw_offset); + auto const handle = fs_get_handle(info); + + return fs->read(path, buffer, buffer_size, offset, handle); +} + +static int fs_write(char const * path, char const * buffer, + size_t buffer_size, off_t raw_offset, + fuse_file_info * info) +{ + auto * const fs = fs_get_filesystem(); + auto const offset = static_cast(raw_offset); + auto const handle = fs_get_handle(info); + + return fs->write(path, buffer, buffer_size, offset, handle); +} + +static int fs_mkdir(char const * path, mode_t mode) +{ + auto * const fs = fs_get_filesystem(); + return fs->mkdir(path, mode); +} + +static int fs_readdir(char const * path, void * buffer, + fuse_fill_dir_t filler, off_t offset, fuse_file_info * info, + fuse_readdir_flags flags) +{ + auto * const fs = fs_get_filesystem(); + std::vector names; + auto const result = fs->readdir(path, names); + if (0 == result) + { + filler(buffer, ".", nullptr, 0, static_cast(0)); + filler(buffer, "..", nullptr, 0, static_cast(0)); + for (auto const & name: names) + { + filler(buffer, name.c_str(), nullptr, 0, static_cast(0)); + } + } + + return result; +} + +static int fs_rmdir(char const * path) +{ + auto * const fs = fs_get_filesystem(); + return fs->rmdir(path); +} + +static int fs_statfs(char const * path, struct statvfs * buffer) +{ + auto * const fs = fs_get_filesystem(); + return fs->statfs(path, buffer); +} + +static int fs_utimes(const char * path, const struct timespec tv[2], struct fuse_file_info * info) +{ + auto * const fs = fs_get_filesystem(); + auto handle = fs_get_handle(info); + return fs->utimens(path, tv, handle); +} + +} + +namespace webfuse +{ + +class fuse::detail +{ +public: + filesystem_i & filesystem; +}; + +fuse::fuse(filesystem_i & filesystem) +: d(new detail{filesystem}) +{ + +} + +fuse::~fuse() +{ + delete d; +} + +fuse::fuse(fuse && other) +{ + this->d = other.d; + other.d = nullptr; +} + +fuse& fuse::operator=(fuse && other) +{ + if (this != &other) + { + delete d; + this->d = other.d; + other.d = nullptr; + } + + return *this; +} + +int fuse::run(int argc, char * argv[]) +{ + void * context = reinterpret_cast(&d->filesystem); + struct fuse_operations operations; + memset(reinterpret_cast(&operations), 0, sizeof(operations)); + operations.init = fs_init; + operations.access = fs_access; + operations.getattr = fs_getattr; + operations.readlink = fs_readlink; + operations.symlink = fs_symlink; + operations.link = fs_link; + operations.rename = fs_rename; + operations.chmod = fs_chmod; + operations.chown = fs_chown; + operations.truncate = fs_truncate; + operations.fsync = fs_fsync; + operations.open = fs_open; + operations.mknod = fs_mknod; + operations.create = fs_create; + operations.release = fs_release; + operations.unlink = fs_unlink; + operations.read = fs_read; + operations.write = fs_write; + operations.mkdir = fs_mkdir; + operations.readdir = fs_readdir; + operations.rmdir = fs_rmdir; + operations.statfs = fs_statfs; + operations.utimens = fs_utimes; + + return fuse_main(argc, argv, &operations, context); +} + +void fuse::print_usage() +{ + struct fuse_operations operations; + memset(reinterpret_cast(&operations), 0, sizeof(operations)); + + int const argc = 2; + char progname[] = "webfuse"; + char show_help[] = "--help"; + char * argv[] = { progname, show_help, nullptr}; + fuse_main(argc, argv, &operations, nullptr); +} + + +} diff --git a/src/webfuse/fuse.hpp b/src/webfuse/fuse.hpp new file mode 100644 index 0000000..b0c1fc5 --- /dev/null +++ b/src/webfuse/fuse.hpp @@ -0,0 +1,27 @@ +#ifndef WEBFUSE_FUSE_HPP +#define WEBFUSE_FUSE_HPP + +#include "webfuse/filesystem/filesystem_i.hpp" + +namespace webfuse +{ + +class fuse +{ + fuse (fuse const &) = delete; + fuse& operator=(fuse const &) = delete; +public: + explicit fuse(filesystem_i & filesystem); + ~fuse(); + fuse (fuse && other); + fuse& operator=(fuse && other); + int run(int argc, char * argv[]); + static void print_usage(); +private: + class detail; + detail * d; +}; + +} + +#endif diff --git a/src/webfuse/provider.cpp b/src/webfuse/provider.cpp new file mode 100644 index 0000000..257aadc --- /dev/null +++ b/src/webfuse/provider.cpp @@ -0,0 +1,437 @@ +#include "webfuse/provider.hpp" +#include "webfuse/ws/client.hpp" + +#include +#include + +#include +#include + +namespace webfuse +{ + +class provider::detail +{ +public: + detail(filesystem_i & fs, std::string const & ca_path) + : fs_(fs) + , client(ca_path, [this](auto& reader) { return this->on_message(reader); }) + { + + } + + ~detail() + { + + } + + void connect(std::string const & url) + { + client.connect(url); + } + + void service() + { + client.service(); + } + + void interrupt() + { + client.interrupt(); + } + + void set_connection_listener(std::function listener) + { + client.set_connection_listener(listener); + } + + messagewriter on_message(messagereader & reader) + { + auto const message_id = reader.read_u32(); + auto const req_type = get_request_type(reader.read_u8()); + auto const resp_type = get_response_type(req_type); + + messagewriter writer(resp_type); + writer.set_id(message_id); + + switch (req_type) + { + case request_type::access: + fs_access(reader, writer); + break; + case request_type::getattr: + fs_getattr(reader, writer); + break; + case request_type::readlink: + fs_readlink(reader, writer); + break; + case request_type::symlink: + fs_symlink(reader, writer); + break; + case request_type::link: + fs_link(reader, writer); + break; + case request_type::rename: + fs_rename(reader, writer); + break; + case request_type::chmod: + fs_chmod(reader, writer); + break; + case request_type::chown: + fs_chown(reader, writer); + break; + case request_type::truncate: + fs_truncate(reader, writer); + break; + case request_type::fsync: + fs_fsync(reader, writer); + break; + case request_type::utimens: + fs_utimens(reader, writer); + break; + case request_type::open: + fs_open(reader, writer); + break; + case request_type::mknod: + fs_mknod(reader, writer); + break; + case request_type::create: + fs_create(reader, writer); + break; + case request_type::release: + fs_release(reader, writer); + break; + case request_type::unlink: + fs_unlink(reader, writer); + break; + case request_type::read: + fs_read(reader, writer); + break; + case request_type::write: + fs_write(reader, writer); + break; + case request_type::mkdir: + fs_mkdir(reader, writer); + break; + case request_type::readdir: + fs_readdir(reader, writer); + break; + case request_type::rmdir: + fs_rmdir(reader, writer); + break; + case request_type::statfs: + fs_statfs(reader, writer); + break; + case request_type::getcreds: + fs_get_credentials(reader, writer); + break; + default: + std::cout << "unknown request: " << ((int) req_type) << std::endl; + break; + } + + return std::move(writer); + } +private: + void fs_access(messagereader & reader, messagewriter & writer) + { + auto const path = reader.read_str(); + auto const mode = reader.read_access_mode(); + + auto const result = fs_.access(path, mode); + writer.write_i32(result); + } + + void fs_getattr(messagereader & reader, messagewriter & writer) + { + auto const path = reader.read_str(); + + struct stat buffer; + auto const result = fs_.getattr(path, &buffer); + writer.write_i32(result); + if (0 == result) + { + writer.write_attr(&buffer); + } + } + + void fs_readlink(messagereader & reader, messagewriter & writer) + { + auto const path = reader.read_str(); + std::string out; + + auto const result = fs_.readlink(path, out); + writer.write_i32(result); + if (0 == result) + { + writer.write_str(out); + } + } + + void fs_symlink(messagereader & reader, messagewriter & writer) + { + auto const from = reader.read_str(); + auto const to = reader.read_str(); + + auto const result = fs_.symlink(from, to); + writer.write_i32(result); + } + + void fs_link(messagereader & reader, messagewriter & writer) + { + auto const from = reader.read_str(); + auto const to = reader.read_str(); + + auto const result = fs_.link(from, to); + writer.write_i32(result); + } + + void fs_rename(messagereader & reader, messagewriter & writer) + { + auto const from = reader.read_str(); + auto const to = reader.read_str(); + auto const flags = reader.read_u8(); + + auto const result = fs_.rename(from, to, flags); + writer.write_i32(result); + } + + void fs_chmod(messagereader & reader, messagewriter & writer) + { + auto const path = reader.read_str(); + auto const mode = reader.read_mode(); + + auto const result = fs_.chmod(path, mode); + writer.write_i32(result); + } + + void fs_chown(messagereader & reader, messagewriter & writer) + { + auto const path = reader.read_str(); + auto const uid = static_cast(reader.read_u32()); + auto const gid = static_cast(reader.read_u32()); + + auto const result = fs_.chown(path, uid, gid); + writer.write_i32(result); + } + + void fs_truncate(messagereader & reader, messagewriter & writer) + { + auto const path = reader.read_str(); + auto const size = reader.read_u64(); + auto const handle = reader.read_u64(); + + auto const result = fs_.truncate(path, size, handle); + writer.write_i32(result); + } + + void fs_fsync(messagereader & reader, messagewriter & writer) + { + auto const path = reader.read_str(); + auto const is_datasync = reader.read_bool(); + auto const handle = reader.read_u64(); + + auto const result = fs_.fsync(path, is_datasync, handle); + writer.write_i32(result); + } + + void fs_utimens(messagereader & reader, messagewriter & writer) + { + auto const path = reader.read_str(); + struct timespec times[2]; + reader.read_time(times[0]); + reader.read_time(times[1]); + auto const handle = reader.read_u64(); + + auto const result = fs_.utimens(path, times, handle); + writer.write_i32(result); + } + + void fs_open(messagereader & reader, messagewriter & writer) + { + auto const path = reader.read_str(); + auto const flags = reader.read_openflags(); + uint64_t handle = static_cast(-1); + + auto const result = fs_.open(path, flags, handle); + writer.write_i32(result); + if (result == 0) + { + writer.write_u64(handle); + } + } + + void fs_mknod(messagereader & reader, messagewriter & writer) + { + auto const path = reader.read_str(); + auto const mode = reader.read_mode(); + auto const dev = reader.read_u32(); + + auto const result = fs_.mknod(path, mode, dev); + writer.write_i32(result); + } + + void fs_create(messagereader & reader, messagewriter & writer) + { + auto const path = reader.read_str(); + auto const mode = reader.read_mode(); + uint64_t handle = static_cast(-1); + + auto const result = fs_.create(path, mode, handle); + writer.write_i32(result); + if (result == 0) + { + writer.write_u64(handle); + } + } + + void fs_release(messagereader & reader, messagewriter & writer) + { + auto const path = reader.read_str(); + auto const handle = reader.read_u64(); + + auto const result = fs_.release(path, handle); + writer.write_i32(result); + } + + void fs_unlink(messagereader & reader, messagewriter & writer) + { + auto const path = reader.read_str(); + + auto const result = fs_.unlink(path); + writer.write_i32(result); + } + + void fs_read(messagereader & reader, messagewriter & writer) + { + auto const path = reader.read_str(); + auto const size = reader.read_u32(); + auto const offset = reader.read_u64(); + auto const handle = reader.read_u64(); + + std::vector buffer(size); + + auto const result = fs_.read(path, buffer.data(), size, offset, handle); + writer.write_i32(result); + if (0 < result) + { + writer.write_data(buffer.data(), result); + } + } + + void fs_write(messagereader & reader, messagewriter & writer) + { + auto const path = reader.read_str(); + auto const buffer = reader.read_bytes(); + auto const offset = reader.read_u64(); + auto const handle = reader.read_u64(); + + auto const result = fs_.write(path, buffer.c_str(), buffer.size(), offset, handle); + writer.write_i32(result); + } + + void fs_mkdir(messagereader & reader, messagewriter & writer) + { + auto const path = reader.read_str(); + auto const mode = reader.read_mode(); + + auto const result = fs_.mkdir(path, mode); + writer.write_i32(result); + } + + void fs_rmdir(messagereader & reader, messagewriter & writer) + { + auto const path = reader.read_str(); + + auto const result = fs_.rmdir(path); + writer.write_i32(result); + } + + void fs_readdir(messagereader & reader, messagewriter & writer) + { + auto const path = reader.read_str(); + std::vector entries; + + auto const result = fs_.readdir(path, entries); + writer.write_i32(result); + if (0 == result) + { + writer.write_strings(entries); + } + } + + void fs_statfs(messagereader & reader, messagewriter & writer) + { + auto const path = reader.read_str(); + struct statvfs statistics; + memset(reinterpret_cast(&statistics), 0, sizeof(statistics)); + + auto const result = fs_.statfs(path, &statistics); + writer.write_i32(result); + if (0 == result) + { + writer.write_statistics(&statistics); + } + } + + void fs_get_credentials(messagereader & reader, messagewriter & writer) + { + std::string credentials = fs_.get_credentials(); + writer.write_str(credentials); + } + + + filesystem_i & fs_; + ws_client client; +}; + +provider::provider(filesystem_i & fs, std::string const & ca_path) +: d(new detail(fs, ca_path)) +{ + +} + +provider::~provider() +{ + delete d; +} + +provider::provider(provider && other) +{ + this->d = other.d; + other.d = nullptr; +} + +provider& provider::operator=(provider && other) +{ + if (this != &other) + { + delete this->d; + this->d = other.d; + other.d = nullptr; + } + + return *this; +} + +void provider::connect(std::string const & url) +{ + d->connect(url); +} + +void provider::service() +{ + d->service(); +} + +void provider::interrupt() +{ + d->interrupt(); +} + +void provider::set_connection_listener(std::function listener) +{ + d->set_connection_listener(listener); +} + +} diff --git a/src/webfuse/provider.hpp b/src/webfuse/provider.hpp new file mode 100644 index 0000000..4748d87 --- /dev/null +++ b/src/webfuse/provider.hpp @@ -0,0 +1,31 @@ +#ifndef WEBFUSE_PROVIDER_I_HPP +#define WEBFUSE_PROVIDER_I_HPP + +#include "webfuse/filesystem/filesystem_i.hpp" +#include +#include + +namespace webfuse +{ + +class provider +{ + provider(provider const &) = delete; + provider& operator=(provider const &) = delete; +public: + provider(filesystem_i & fs, std::string const & ca_path); + ~provider(); + provider(provider && other); + provider& operator=(provider && other); + void set_connection_listener(std::function listener); + void connect(std::string const & url); + void service(); + void interrupt(); +private: + class detail; + detail * d; +}; + +} + +#endif diff --git a/src/webfuse/request_type.cpp b/src/webfuse/request_type.cpp new file mode 100644 index 0000000..6c35893 --- /dev/null +++ b/src/webfuse/request_type.cpp @@ -0,0 +1,38 @@ +#include "webfuse/request_type.hpp" + +namespace webfuse +{ + +request_type get_request_type(uint8_t value) +{ + switch (value) + { + case static_cast(request_type::access): return request_type::access; + case static_cast(request_type::getattr): return request_type::getattr; + case static_cast(request_type::readlink): return request_type::readlink; + case static_cast(request_type::symlink): return request_type::symlink; + case static_cast(request_type::link): return request_type::link; + case static_cast(request_type::rename): return request_type::rename; + case static_cast(request_type::chmod): return request_type::chmod; + case static_cast(request_type::chown): return request_type::chown; + case static_cast(request_type::truncate): return request_type::truncate; + case static_cast(request_type::fsync): return request_type::fsync; + case static_cast(request_type::open): return request_type::open; + case static_cast(request_type::mknod): return request_type::mknod; + case static_cast(request_type::create): return request_type::create; + case static_cast(request_type::release): return request_type::release; + case static_cast(request_type::unlink): return request_type::unlink; + case static_cast(request_type::read): return request_type::read; + case static_cast(request_type::write): return request_type::write; + case static_cast(request_type::mkdir): return request_type::mkdir; + case static_cast(request_type::readdir): return request_type::readdir; + case static_cast(request_type::rmdir): return request_type::rmdir; + case static_cast(request_type::statfs): return request_type::statfs; + case static_cast(request_type::utimens): return request_type::utimens; + case static_cast(request_type::getcreds): return request_type::getcreds; + default: + return request_type::unknown; + } +} + +} diff --git a/src/webfuse/request_type.hpp b/src/webfuse/request_type.hpp new file mode 100644 index 0000000..a66d763 --- /dev/null +++ b/src/webfuse/request_type.hpp @@ -0,0 +1,41 @@ +#ifndef WEBFUSE_REQUEST_TYPE +#define WEBFUSE_REQUEST_TYPE + +#include + +namespace webfuse +{ + +enum class request_type: uint8_t +{ + unknown = 0x00, + access = 0x01, + getattr = 0x02, + readlink = 0x03, + symlink = 0x04, + link = 0x05, + rename = 0x06, + chmod = 0x07, + chown = 0x08, + truncate = 0x09, + fsync = 0x0a, + open = 0x0b, + mknod = 0x0c, + create = 0x0d, + release = 0x0e, + unlink = 0x0f, + read = 0x10, + write = 0x11, + mkdir = 0x12, + readdir = 0x13, + rmdir = 0x14, + statfs = 0x15, + utimens = 0x16, + getcreds = 0x17 +}; + +request_type get_request_type(uint8_t value); + +} + +#endif diff --git a/src/webfuse/response_type.cpp b/src/webfuse/response_type.cpp new file mode 100644 index 0000000..9142ea4 --- /dev/null +++ b/src/webfuse/response_type.cpp @@ -0,0 +1,39 @@ +#include "webfuse/response_type.hpp" + +namespace webfuse +{ + +response_type get_response_type(request_type value) +{ + switch (value) + { + case request_type::access: return response_type::access; + case request_type::getattr: return response_type::getattr; + case request_type::readlink: return response_type::readlink; + case request_type::symlink: return response_type::symlink; + case request_type::link: return response_type::link; + case request_type::rename: return response_type::rename; + case request_type::chmod: return response_type::chmod; + case request_type::chown: return response_type::chown; + case request_type::truncate: return response_type::truncate; + case request_type::fsync: return response_type::fsync; + case request_type::open: return response_type::open; + case request_type::mknod: return response_type::mknod; + case request_type::create: return response_type::create; + case request_type::release: return response_type::release; + case request_type::unlink: return response_type::unlink; + case request_type::read: return response_type::read; + case request_type::write: return response_type::write; + case request_type::mkdir: return response_type::mkdir; + case request_type::readdir: return response_type::readdir; + case request_type::rmdir: return response_type::rmdir; + case request_type::statfs: return response_type::statfs; + case request_type::utimens: return response_type::utimens; + case request_type::getcreds: return response_type::getcreds; + default: + return response_type::unknown; + } +} + + +} diff --git a/src/webfuse/response_type.hpp b/src/webfuse/response_type.hpp new file mode 100644 index 0000000..d413714 --- /dev/null +++ b/src/webfuse/response_type.hpp @@ -0,0 +1,42 @@ +#ifndef WEBFUSE_RESPONSE_TYPE +#define WEBFUSE_RESPONSE_TYPE + +#include "request_type.hpp" +#include + +namespace webfuse +{ + +enum class response_type: uint8_t +{ + unknown = 0x80, + access = 0x81, + getattr = 0x82, + readlink = 0x83, + symlink = 0x84, + link = 0x85, + rename = 0x86, + chmod = 0x87, + chown = 0x88, + truncate = 0x89, + fsync = 0x8a, + open = 0x8b, + mknod = 0x8c, + create = 0x8d, + release = 0x8e, + unlink = 0x8f, + read = 0x90, + write = 0x91, + mkdir = 0x92, + readdir = 0x93, + rmdir = 0x94, + statfs = 0x95, + utimens = 0x96, + getcreds = 0x97 +}; + +response_type get_response_type(request_type value); + +} + +#endif diff --git a/src/webfuse/util/authenticator.cpp b/src/webfuse/util/authenticator.cpp new file mode 100644 index 0000000..579aaab --- /dev/null +++ b/src/webfuse/util/authenticator.cpp @@ -0,0 +1,67 @@ +#include "webfuse/util/authenticator.hpp" + +#include +#include +#include + +namespace webfuse +{ + +authenticator::authenticator(std::string const & app) +: app_(app) +{ + +} + +bool authenticator::authenticate(std::string const & token) +{ + bool result = false; + + int fds[2]; + int const rc = pipe(fds); + if (0 != rc) + { + return false; + } + + pid_t const pid = fork(); + + if (pid == 0) + { + // child + + close(STDIN_FILENO); + dup2(fds[0], STDIN_FILENO); + + // prepare file descriptors + closefrom(1); + open("/dev/null", O_WRONLY); + dup2(STDOUT_FILENO, STDERR_FILENO); + + execl(app_.c_str(), app_.c_str(), nullptr); + exit(EXIT_FAILURE); + } + else if (pid > 0) + { + write(fds[1], reinterpret_cast(token.c_str()), token.size()); + close(fds[1]); + + // parent + int exit_status = EXIT_FAILURE; + + int status = 0; + int const rc = waitpid(pid, &status, 0); + if (rc == pid) + { + exit_status = WEXITSTATUS(status); + } + + close(fds[0]); + result = (exit_status == EXIT_SUCCESS); + } + + return result; +} + + +} diff --git a/src/webfuse/util/authenticator.hpp b/src/webfuse/util/authenticator.hpp new file mode 100644 index 0000000..348deb9 --- /dev/null +++ b/src/webfuse/util/authenticator.hpp @@ -0,0 +1,23 @@ +#ifndef WEBFUSE_AUTHENTICATOR_HPP +#define WEBFUSE_AUTHENTICATOR_HPP + +#include + +namespace webfuse +{ + +class authenticator +{ +public: + explicit authenticator(std::string const & app); + ~authenticator() = default; + + bool authenticate(std::string const & token); + +private: + std::string app_; +}; + +} + +#endif diff --git a/src/webfuse/util/commandline_args.cpp b/src/webfuse/util/commandline_args.cpp new file mode 100644 index 0000000..a31fab9 --- /dev/null +++ b/src/webfuse/util/commandline_args.cpp @@ -0,0 +1,56 @@ +#include "webfuse/util/commandline_args.hpp" +#include +#include + +namespace webfuse +{ + +commandline_args::commandline_args(char const * prog_name, int capacity) +: capacity_(capacity) +, argc(0) +{ + if (capacity < 1) + { + throw std::runtime_error("too few commandline args"); + } + + argv = new char*[capacity_ + 1]; + argv[0] = nullptr; + push(prog_name); +} + +commandline_args::~commandline_args() +{ + for(int i = 0; i < argc; i++) + { + free(argv[i]); + } + delete[] argv; +} + +void commandline_args::push(char const * arg) +{ + if (argc < capacity_) + { + argv[argc] = strdup(arg); + argv[argc + 1] = nullptr; + argc++; + } + else + { + throw std::runtime_error("capacity exceeded"); + } +} + +int commandline_args::get_argc() const +{ + return argc; +} + +char ** commandline_args::get_argv() +{ + return argv; +} + + +} diff --git a/src/webfuse/util/commandline_args.hpp b/src/webfuse/util/commandline_args.hpp new file mode 100644 index 0000000..9c9b631 --- /dev/null +++ b/src/webfuse/util/commandline_args.hpp @@ -0,0 +1,29 @@ +#ifndef WEBFUSE_COMMANDLINE_ARGS_HPP +#define WEBFUSE_COMMANDLINE_ARGS_HPP + +namespace webfuse +{ + +class commandline_args +{ + commandline_args (commandline_args const &) = delete; + commandline_args& operator=(commandline_args const &) = delete; + commandline_args (commandline_args &&) = delete; + commandline_args& operator=(commandline_args &&) = delete; +public: + commandline_args(char const * prog_name, int capacity); + ~commandline_args(); + + void push(char const * arg); + int get_argc() const; + char ** get_argv(); + +private: + int capacity_; + int argc; + char ** argv; +}; + +} + +#endif diff --git a/src/webfuse/util/commandline_reader.cpp b/src/webfuse/util/commandline_reader.cpp new file mode 100644 index 0000000..db5430f --- /dev/null +++ b/src/webfuse/util/commandline_reader.cpp @@ -0,0 +1,30 @@ +#include "webfuse/util/commandline_reader.hpp" +namespace webfuse +{ + +commandline_reader::commandline_reader(int argc, char * argv[]) +: current_(0) +, argc_(argc) +, argv_(argv) +{ + +} + +bool commandline_reader::next() +{ + if (current_ < argc_) + { + current_++; + } + + bool const has_next = (current_ < argc_); + return has_next; +} + +char const * commandline_reader::current() const +{ + return argv_[current_]; +} + + +} diff --git a/src/webfuse/util/commandline_reader.hpp b/src/webfuse/util/commandline_reader.hpp new file mode 100644 index 0000000..252f869 --- /dev/null +++ b/src/webfuse/util/commandline_reader.hpp @@ -0,0 +1,22 @@ +#ifndef WEBFUSE_COMMANDLINE_READER_HPP +#define WEBFUSE_COMMANDLINE_READER_HPP + +namespace webfuse +{ + +class commandline_reader +{ +public: + commandline_reader(int argc, char * argv[]); + ~commandline_reader() = default; + bool next(); + char const * current() const; +private: + int current_; + int argc_; + char ** argv_; +}; + +} + +#endif diff --git a/src/webfuse/version.cpp.in b/src/webfuse/version.cpp.in new file mode 100644 index 0000000..f77e3cf --- /dev/null +++ b/src/webfuse/version.cpp.in @@ -0,0 +1,11 @@ +#include "webfuse/version.hpp" + +namespace webfuse +{ + +char const * get_version() +{ + return "@webfuse_VERSION@"; +} + +} diff --git a/src/webfuse/version.hpp b/src/webfuse/version.hpp new file mode 100644 index 0000000..ad540a8 --- /dev/null +++ b/src/webfuse/version.hpp @@ -0,0 +1,12 @@ +#ifndef WEBFUSE_VERSION_HPP +#define WEBFUSE_VERSION_HPP + +namespace webfuse +{ + +char const * get_version(); + + +} + +#endif diff --git a/src/webfuse/webfuse.cpp b/src/webfuse/webfuse.cpp new file mode 100644 index 0000000..58c1799 --- /dev/null +++ b/src/webfuse/webfuse.cpp @@ -0,0 +1,54 @@ +#include "webfuse/webfuse.hpp" +#include "webfuse/fuse.hpp" +#include "webfuse/filesystem.hpp" +#include "webfuse/ws/server.hpp" +#include "webfuse/version.hpp" + +#include + +namespace webfuse +{ + +int app::run(int argc, char * argv[]) // NOLINT(readability-convert-member-functions-to-static) +{ + ws_config config(argc, argv); + + switch (config.cmd) + { + case command::run: + { + ws_server server(config); + filesystem filesystem(server); + fuse fuse_fs(filesystem); + + config.exit_code = fuse_fs.run(config.args.get_argc(), config.args.get_argv()); + } + break; + case command::print_version: + std::cout << webfuse::get_version() << std::endl; + break; + case command::show_help: + // fall-through + default: + { + fuse::print_usage(); + std::cout << R"( +WEBFUSE options: + --wf-port PORT port number of websocket server (default: 8081) + --wf-vhost VHOST name of the virtual host (default: localhost) + --wf-cert PATH path of the server's public certificate (optional) + --wf-key PATH path of the server's private key (optional) + --wf-authenticator PATH path of authenticatior app (optional) + --wf-auth-header NAME name of the authentication header (optional) + --wf-docroot PATH enables HTTP server with given document root (optional) + --wf-timeout TIMEOUT communication timeout in seconds (default: 10) + --wf-version print version and exit +)"; + } + break; + } + + return config.exit_code; +} + +} diff --git a/src/webfuse/webfuse.hpp b/src/webfuse/webfuse.hpp new file mode 100644 index 0000000..8738cc6 --- /dev/null +++ b/src/webfuse/webfuse.hpp @@ -0,0 +1,15 @@ +#ifndef WEBFUSE_HPP +#define WEBFUSE_HPP + +namespace webfuse +{ + +class app +{ +public: + int run(int argc, char * argv[]); +}; + +} + +#endif diff --git a/src/webfuse/ws/client.cpp b/src/webfuse/ws/client.cpp new file mode 100644 index 0000000..1f488fb --- /dev/null +++ b/src/webfuse/ws/client.cpp @@ -0,0 +1,240 @@ +#include "webfuse/ws/client.hpp" +#include "webfuse/ws/url.hpp" + +#include +#include +#include + +#include +#include + +namespace +{ + +struct user_data +{ + webfuse::ws_client_handler handler; + std::function connection_listener; + struct lws * connection; + std::string current_message; + std::queue requests; +}; + +extern "C" int webfuse_client_callback(lws * wsi, lws_callback_reasons reason, void* user, void * in, size_t length) +{ + int result = 0; + + lws_protocols const * protocol = lws_get_protocol(wsi); + user_data * context = (nullptr != protocol) ? reinterpret_cast(protocol->user) : nullptr; + + if (nullptr != context) + { + switch(reason) + { + case LWS_CALLBACK_CLIENT_ESTABLISHED: + context->connection_listener(true); + break; + case LWS_CALLBACK_CLIENT_CONNECTION_ERROR: + context->connection = nullptr; + context->requests = std::move(std::queue()); + context->current_message.clear(); + context->connection_listener(false); + break; + case LWS_CALLBACK_CLIENT_CLOSED: + context->connection = nullptr; + context->requests = std::move(std::queue()); + context->current_message.clear(); + context->connection_listener(false); + break; + case LWS_CALLBACK_CLIENT_RECEIVE: + { + auto * fragment = reinterpret_cast(in); + context->current_message.append(fragment, length); + if (0 != lws_is_final_fragment(wsi)) + { + try + { + webfuse::messagereader reader(context->current_message); + auto writer = context->handler(reader); + + context->requests.emplace(std::move(writer)); + lws_callback_on_writable(wsi); + } + catch(...) + { + // ToDo: log + std::cerr << "error: failed to create response" << std::endl; + } + } + } + break; + case LWS_CALLBACK_SERVER_WRITEABLE: + // fall-through + case LWS_CALLBACK_CLIENT_WRITEABLE: + { + if (!context->requests.empty()) + { + auto writer = std::move(context->requests.front()); + context->requests.pop(); + + size_t size; + auto * data = writer.get_data(size); + lws_write(wsi, data, size, LWS_WRITE_BINARY); + } + + if (!context->requests.empty()) + { + lws_callback_on_writable(wsi); + } + } + break; + default: + break; + } + } + + return result; +} + +} + +namespace webfuse +{ + +class ws_client::detail +{ + detail(detail const &) = delete; + detail& operator=(detail const &) = delete; + detail(detail &&) = delete; + detail& operator=(detail &&) = delete; +public: + detail(std::string const & ca_path, ws_client_handler handler) + { + lws_set_log_level(0, nullptr); + + memset(reinterpret_cast(protocols), 0, sizeof(lws_protocols) * 2); + protocols[0].callback = &webfuse_client_callback; + protocols[0].name = "webfuse2-client"; + protocols[0].per_session_data_size = 0; + protocols[0].user = reinterpret_cast(&data); + + memset(reinterpret_cast(&info), 0, sizeof(lws_context_creation_info)); + info.port = CONTEXT_PORT_NO_LISTEN; + info.protocols = protocols; + info.uid = -1; + info.gid = -1; + info.options = LWS_SERVER_OPTION_EXPLICIT_VHOSTS; + + data.handler = handler; + data.connection_listener = [](bool){ }; + data.connection = nullptr; + + context = lws_create_context(&info); + + struct lws_vhost * vhost = lws_create_vhost(context, &info); + info.options |= LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT; + if (!ca_path.empty()) + { + info.client_ssl_ca_filepath = ca_path.c_str(); + } + + lws_init_vhost_client_ssl(&info, vhost); + } + + ~detail() + { + lws_context_destroy(context); + } + + void connect(std::string const & url) + { + ws_url parsed_url(url); + + lws_client_connect_info info; + memset(reinterpret_cast(&info), 0, sizeof(lws_client_connect_info)); + info.context = context; + info.port = parsed_url.port; + info.address = parsed_url.hostname.c_str(); + info.host = info.address; + info.path = parsed_url.path.c_str(); + info.origin = info.address; + info.ssl_connection = (parsed_url.use_tls) ? LCCSCF_USE_SSL : 0; + info.protocol = "webfuse2"; + info.local_protocol_name = "webfuse2-client"; + info.pwsi = &data.connection; + + lws_client_connect_via_info(&info); + } + + void service() + { + lws_service(context, 0); + } + + void interrupt() + { + lws_cancel_service(context); + } + + void set_connection_listener(std::function listener) + { + data.connection_listener = listener; + } + +private: + lws_context_creation_info info; + lws_protocols protocols[2]; + lws_context * context; + user_data data; +}; + +ws_client::ws_client(std::string const & ca_path, ws_client_handler handler) +: d(new detail(ca_path, handler)) +{ + +} + +ws_client::~ws_client() +{ + delete d; +} + +ws_client::ws_client(ws_client && other) +{ + this->d = other.d; + other.d = nullptr; +} + +ws_client& ws_client::operator=(ws_client && other) +{ + if (this != &other) + { + delete this->d; + this->d = other.d; + other.d = nullptr; + } + + return *this; +} + +void ws_client::connect(std::string url) +{ + d->connect(url); +} + +void ws_client::service() +{ + d->service(); +} + +void ws_client::interrupt() +{ + d->interrupt(); +} + +void ws_client::set_connection_listener(std::function listener) +{ + d->set_connection_listener(listener); +} + +} diff --git a/src/webfuse/ws/client.hpp b/src/webfuse/ws/client.hpp new file mode 100644 index 0000000..0fa3161 --- /dev/null +++ b/src/webfuse/ws/client.hpp @@ -0,0 +1,35 @@ +#ifndef WEBFUSE_WSCLIENT_HPP +#define WEBFUSE_WSCLIENT_HPP + +#include "webfuse/ws/messagewriter.hpp" +#include "webfuse/ws/messagereader.hpp" + +#include + +namespace webfuse +{ + +using ws_client_handler = std::function; + +class ws_client +{ + ws_client(ws_client const &) = delete; + ws_client& operator=(ws_client const &) = delete; +public: + ws_client(std::string const & ca_path, ws_client_handler handler); + ~ws_client(); + ws_client(ws_client && other); + ws_client& operator=(ws_client && other); + + void set_connection_listener(std::function listener); + void connect(std::string url); + void service(); + void interrupt(); +private: + class detail; + detail * d; +}; + +} + +#endif diff --git a/src/webfuse/ws/config.cpp b/src/webfuse/ws/config.cpp new file mode 100644 index 0000000..2a35f4d --- /dev/null +++ b/src/webfuse/ws/config.cpp @@ -0,0 +1,133 @@ +#include "webfuse/ws/config.hpp" +#include "webfuse/util/commandline_reader.hpp" + +#include +#include +#include + +namespace +{ + +constexpr int const default_port = 8081; +constexpr char const default_vhost_name[] = "localhost"; +constexpr uint64_t const default_timeout_secs = 10; + +void verify(webfuse::ws_config & config) +{ + if (config.cmd == webfuse::command::run) + { + if ((config.use_tls) && ((config.key_path.empty()) || (config.cert_path.empty()))) + { + std::cerr << "error: use of TLS requires both, key ander certificate path" << std::endl; + config.cmd = webfuse::command::show_help; + config.exit_code = EXIT_FAILURE; + } + } +} + +bool get_arg(webfuse::ws_config & config, webfuse::commandline_reader& reader, std::string & value, std::string const error_message) +{ + const bool has_next = reader.next(); + if (has_next) + { + value = reader.current(); + } + else + { + std::cerr << "error: " << error_message << std::endl; + config.cmd = webfuse::command::show_help; + config.exit_code = EXIT_FAILURE; + } + + return has_next; +} + +} + +namespace webfuse +{ + +// NOLINTNEXTLINE +ws_config::ws_config(int argc, char * argv[]) +: exit_code(EXIT_SUCCESS) +, args(argv[0], argc) +, cmd(command::run) +, port(default_port) +, vhost_name(default_vhost_name) +, use_tls(false) +, timeout_secs(default_timeout_secs) +{ + commandline_reader reader(argc, argv); + + while ((exit_code == EXIT_SUCCESS) && (reader.next())) + { + std::string const arg = reader.current(); + if ((arg == "-h") || (arg == "--help")) + { + cmd = command::show_help; + } + else if (arg == "--wf-port") + { + std::string value; + if (get_arg(*this, reader, value, "missing PORT")) + { + port = static_cast(std::stoi(value)); + } + } + else if (arg == "--wf-vhost") + { + get_arg(*this, reader, vhost_name, "missing VHOST"); + } + else if (arg == "--wf-key") + { + if (get_arg(*this, reader, key_path, "missing KEY_PATH")) + { + use_tls = true; + } + } + else if (arg == "--wf-cert") + { + if (get_arg(*this, reader, cert_path, "missing CERT_PATH")) + { + use_tls = true; + } + } + else if (arg == "--wf-authenticator") + { + get_arg(*this, reader, authenticator, "missing AUTHENTICATOR"); + } + else if (arg == "--wf-auth-header") + { + if (get_arg(*this, reader, auth_header, "missing AUTH_HEADER")) + { + std::transform(auth_header.begin(), auth_header.end(), auth_header.begin(), + [](auto c) {return std::tolower(c); }); + } + } + else if (arg == "--wf-timeout") + { + std::string timeout_str; + if (get_arg(*this, reader, timeout_str, "missing TIMEOUT")) + { + timeout_secs = static_cast(std::stoi(timeout_str)); + } + } + else if (arg == "--wf-docroot") + { + get_arg(*this, reader, docroot, "missing DOCROOT"); + } + else if (arg == "--wf-version") + { + cmd = command::print_version; + } + else + { + args.push(arg.c_str()); + } + } + + verify(*this); +} + + +} diff --git a/src/webfuse/ws/config.hpp b/src/webfuse/ws/config.hpp new file mode 100644 index 0000000..068fb4a --- /dev/null +++ b/src/webfuse/ws/config.hpp @@ -0,0 +1,44 @@ +#ifndef WEBFUSE_WS_CONFIG_HPP +#define WEBFUSE_WS_CONFIG_HPP + +#include + +#include +#include + +namespace webfuse +{ + +enum class command +{ + run, + print_version, + show_help +}; + +class ws_config +{ +public: + ws_config(int argc, char * argv[]); + + int exit_code; + commandline_args args; + command cmd; + + uint16_t port; + std::string vhost_name; + std::string docroot; + + bool use_tls; + std::string cert_path; + std::string key_path; + + std::string authenticator; + std::string auth_header; + + uint64_t timeout_secs; +}; + +} + +#endif diff --git a/src/webfuse/ws/messagereader.cpp b/src/webfuse/ws/messagereader.cpp new file mode 100644 index 0000000..c78aa6d --- /dev/null +++ b/src/webfuse/ws/messagereader.cpp @@ -0,0 +1,195 @@ +#include "webfuse/ws/messagereader.hpp" +#include "webfuse/filesystem/status.hpp" +#include "webfuse/filesystem/filemode.hpp" +#include "webfuse/filesystem/accessmode.hpp" +#include "webfuse/filesystem/openflags.hpp" + +#include + +namespace webfuse +{ + +messagereader::messagereader(std::string & value) +: data(std::move(value)) +, pos(0) +{ +} + +messagereader::messagereader(messagereader && other) +{ + this->data = std::move(other.data); + this->pos = other.pos; +} + +messagereader& messagereader::operator=(messagereader && other) +{ + if (this != &other) + { + this->data = std::move(other.data); + this->pos = other.pos; + } + + return *this; +} + + +int messagereader::read_result() +{ + status value(read_i32()); + return value.to_fusestatus(); +} + +int messagereader::read_access_mode() +{ + auto const value = read_u8(); + access_mode mode(static_cast(value)); + + return mode.to_int(); +} + +void messagereader::read_attr(struct stat * attr) +{ + attr->st_ino = static_cast(read_u64()); + attr->st_nlink = static_cast(read_u64()); + attr->st_mode = read_mode(); + attr->st_uid = static_cast(read_u32()); + attr->st_gid = static_cast(read_u32()); + attr->st_rdev = static_cast(read_u64()); + attr->st_size = static_cast(read_u64()); + attr->st_blocks = static_cast(read_u64()); + attr->st_atim.tv_sec = static_cast(read_u64()); + attr->st_atim.tv_nsec = static_cast(read_u32()); + attr->st_mtim.tv_sec = static_cast(read_u64()); + attr->st_mtim.tv_nsec = static_cast(read_u32()); + attr->st_ctim.tv_sec = static_cast(read_u64()); + attr->st_ctim.tv_nsec = static_cast(read_u32()); +} + +void messagereader::read_statistics(struct statvfs * statistics) +{ + statistics->f_bsize = read_u64(); + statistics->f_frsize = read_u64(); + statistics->f_blocks = read_u64(); + statistics->f_bfree = read_u64(); + statistics->f_bavail = read_u64(); + statistics->f_files = read_u64(); + statistics->f_ffree = read_u64(); + statistics->f_namemax = read_u64(); +} + + +mode_t messagereader::read_mode() +{ + filemode mode(read_u32()); + return mode.to_mode(); +} + +bool messagereader::read_bool() +{ + return (1 == read_u8()); +} + + +uint8_t messagereader::read_u8() +{ + if (pos < data.size()) + { + uint8_t value = static_cast(data[pos]); + pos++; + return value; + } + + throw std::runtime_error("out of bounds"); +} + +uint32_t messagereader::read_u32() +{ + // NOLINTBEGIN(readability-magic-numbers) + if ((pos + 3) < data.size()) + { + uint32_t value = + ((static_cast(data[pos ]) & 0xff) << 24) | + ((static_cast(data[pos + 1]) & 0xff) << 16) | + ((static_cast(data[pos + 2]) & 0xff) << 8) | + ((static_cast(data[pos + 3]) & 0xff) ); + pos += 4; + return value; + } + // NOLINTEND(readability-magic-numbers) + + throw std::runtime_error("out of bounds"); +} + +uint64_t messagereader::read_u64() +{ + // NOLINTBEGIN(readability-magic-numbers) + if ((pos + 7) < data.size()) + { + uint32_t value = + (static_cast(data[pos ] & 0xff) << 56) | + (static_cast(data[pos + 1] & 0xff) << 48) | + (static_cast(data[pos + 2] & 0xff) << 40) | + (static_cast(data[pos + 3] & 0xff) << 32) | + (static_cast(data[pos + 4] & 0xff) << 24) | + (static_cast(data[pos + 5] & 0xff) << 16) | + (static_cast(data[pos + 6] & 0xff) << 8) | + (static_cast(data[pos + 7] & 0xff) ); + pos += 8; + return value; + } + // NOLINTEND(readability-magic-numbers) + + throw std::runtime_error("out of bounds"); +} + + +int32_t messagereader::read_i32() +{ + uint32_t value = read_u32(); + return static_cast(value); +} + +std::string messagereader::read_str() +{ + return std::move(read_bytes()); +} + +std::string messagereader::read_bytes() +{ + uint32_t size = read_u32(); + if ((pos + size) <= data.size()) + { + std::string const value(&data[pos], size); + pos += size; + return std::move(value); + } + + throw std::runtime_error("out of bounds"); +} + +void messagereader::read_strings(std::vector &entries) +{ + uint32_t const count = read_u32(); + for(uint32_t i = 0; i < count; i++) + { + std::string entry = read_str(); + entries.push_back(entry); + } +} + +void messagereader::read_time(struct timespec &time) +{ + time.tv_sec = static_cast(read_u64()); + time.tv_nsec = static_cast(read_u32()); +} + +int messagereader::read_openflags() +{ + auto const value = read_i32(); + openflags flags(value); + + return flags.to_int(); +} + + +} diff --git a/src/webfuse/ws/messagereader.hpp b/src/webfuse/ws/messagereader.hpp new file mode 100644 index 0000000..de4720c --- /dev/null +++ b/src/webfuse/ws/messagereader.hpp @@ -0,0 +1,53 @@ +#ifndef WEBFUSE_MESSAGEREADER_HPP +#define WEBFUSE_MESSAGEREADER_HPP + +#include +#include +#include +#include + +#include +#include +#include + +namespace webfuse +{ + +class messagereader +{ + messagereader(messagereader const &) = delete; + messagereader& operator=(messagereader const &) = delete; +public: + explicit messagereader(std::string & value); + ~messagereader() = default; + messagereader(messagereader && other); + messagereader& operator=(messagereader && other); + + int read_result(); + void read_attr(struct stat * attr); + void read_statistics(struct statvfs * statistics); + int read_access_mode(); + mode_t read_mode(); + + bool read_bool(); + uint8_t read_u8(); + uint32_t read_u32(); + uint64_t read_u64(); + + int32_t read_i32(); + + std::string read_str(); + std::string read_bytes(); + + void read_strings(std::vector &entries); + void read_time(struct timespec &time); + int read_openflags(); + +private: + std::string data; + size_t pos; +}; + +} + +#endif diff --git a/src/webfuse/ws/messagewriter.cpp b/src/webfuse/ws/messagewriter.cpp new file mode 100644 index 0000000..37328ce --- /dev/null +++ b/src/webfuse/ws/messagewriter.cpp @@ -0,0 +1,223 @@ +#include "webfuse/ws/messagewriter.hpp" +#include "webfuse/filesystem/accessmode.hpp" +#include "webfuse/filesystem/filemode.hpp" +#include "webfuse/filesystem/openflags.hpp" + +#include + +namespace webfuse +{ + +constexpr uint8_t const rename_noreplace = 0x01; +constexpr uint8_t const rename_exchange = 0x02; + +messagewriter::messagewriter(request_type req_type) +: id(0) +, data(LWS_PRE) +{ + write_u32(0); + write_u8(static_cast(req_type)); +} + +messagewriter::messagewriter(response_type res_type) +: id(0) +, data(LWS_PRE) +{ + write_u32(0); + write_u8(static_cast(res_type)); +} + +messagewriter::messagewriter(messagewriter && other) +{ + this->id = other.id; + this->data = std::move(other.data); +} + +messagewriter& messagewriter::operator=(messagewriter && other) +{ + if (this != &other) + { + this->id = other.id; + this->data = std::move(other.data); + } + + return *this; +} + +void messagewriter::set_id(uint32_t value) +{ + id = value; + + // NOLINTBEGIN(readability-magic-numbers) + data[LWS_PRE ] = (id >> 24) & 0xff; + data[LWS_PRE + 1] = (id >> 16) & 0xff; + data[LWS_PRE + 2] = (id >> 8) & 0xff; + data[LWS_PRE + 3] = id & 0xff; + // NOLINTEND(readability-magic-numbers) +} + +uint32_t messagewriter::get_id() const +{ + return id; +} + +void messagewriter::write_bool(bool value) +{ + data.push_back(value ? 0x01 : 0x00); +} + +void messagewriter::write_u8(uint8_t value) +{ + data.push_back(value); +} + +void messagewriter::write_i8(int8_t value) +{ + data.push_back(static_cast(value)); +} + +void messagewriter::write_i32(int32_t value) +{ + write_u32((static_cast(value))); +} + +void messagewriter::write_u32(uint32_t value) +{ + auto const offset = data.size(); + + // NOLINTBEGIN(readability-magic-numbers) + data.resize(offset + 4); + data[offset ] = (value >> 24) & 0xff; + data[offset + 1] = (value >> 16) & 0xff; + data[offset + 2] = (value >> 8) & 0xff; + data[offset + 3] = value & 0xff; + // NOLINTEND(readability-magic-numbers) +} + +void messagewriter::write_u64(uint64_t value) +{ + auto const offset = data.size(); + + // NOLINTBEGIN(readability-magic-numbers) + data.resize(offset + 8); + data[offset ] = (value >> 56) & 0xff; + data[offset + 1] = (value >> 48) & 0xff; + data[offset + 2] = (value >> 40) & 0xff; + data[offset + 3] = (value >> 32) & 0xff; + data[offset + 4] = (value >> 24) & 0xff; + data[offset + 5] = (value >> 16) & 0xff; + data[offset + 6] = (value >> 8) & 0xff; + data[offset + 7] = value & 0xff; + // NOLINTEND(readability-magic-numbers) +} + +void messagewriter::write_str(std::string const &value) +{ + write_data(value.data(), value.size()); +} + +void messagewriter::write_data(char const * buffer, size_t size) +{ + uint32_t const effective_size = size & 0xffffffff; + write_u32(effective_size); + + if (size > 0) + { + auto const offset = data.size(); + data.resize(offset + effective_size); + void * to = reinterpret_cast(&data[offset]); + void const * from = reinterpret_cast(buffer); + memcpy(to, from, effective_size); + } +} + +void messagewriter::write_strings(std::vector const & list) +{ + uint32_t const count = list.size() & 0xffffffff; + write_u32(count); + for (auto const & item: list) + { + write_str(item); + } +} + +void messagewriter::write_attr(struct stat const * attr) +{ + write_u64(static_cast(attr->st_ino)); + write_u64(static_cast(attr->st_nlink)); + write_mode(filemode::from_mode(attr->st_mode)); + write_u32(static_cast(attr->st_uid)); + write_u32(static_cast(attr->st_gid)); + write_u64(static_cast(attr->st_rdev)); + write_u64(static_cast(attr->st_size)); + write_u64(static_cast(attr->st_blocks)); + write_time(attr->st_atim); + write_time(attr->st_mtim); + write_time(attr->st_ctim); +} + +void messagewriter::write_access_mode(int value) +{ + access_mode mode = access_mode::from_int(value); + write_i8(mode); +} + +void messagewriter::write_rename_flags(unsigned int value) +{ + uint8_t flags = 0; + if (RENAME_NOREPLACE == (value & RENAME_NOREPLACE)) { flags |= rename_noreplace; } + if (RENAME_EXCHANGE == (value & RENAME_EXCHANGE )) { flags |= rename_exchange; } + + write_u8(flags); +} + +void messagewriter::write_mode(mode_t value) +{ + filemode mode = filemode::from_mode(value); + write_u32(mode); +} + +void messagewriter::write_uid(uid_t value) +{ + write_u32(static_cast(value)); +} + +void messagewriter::write_gid(gid_t value) +{ + write_u32(static_cast(value)); +} + +void messagewriter::write_openflags(int value) +{ + openflags flags = openflags::from_int(value); + write_i32(flags); +} + +void messagewriter::write_time(timespec const & value) +{ + write_u64(static_cast(value.tv_sec)); + write_u32(static_cast(value.tv_nsec)); +} + +void messagewriter::write_statistics(struct statvfs const * statistics) +{ + write_u64(statistics->f_bsize); + write_u64(statistics->f_frsize); + write_u64(statistics->f_blocks); + write_u64(statistics->f_bfree); + write_u64(statistics->f_bavail); + write_u64(statistics->f_files); + write_u64(statistics->f_ffree); + write_u64(statistics->f_namemax); +} + +unsigned char * messagewriter::get_data(size_t &size) +{ + size = data.size() - LWS_PRE; + void * result = reinterpret_cast(&data[LWS_PRE]); + + return reinterpret_cast(result); +} + + +} diff --git a/src/webfuse/ws/messagewriter.hpp b/src/webfuse/ws/messagewriter.hpp new file mode 100644 index 0000000..b89a327 --- /dev/null +++ b/src/webfuse/ws/messagewriter.hpp @@ -0,0 +1,60 @@ +#ifndef WEBFUSE_MESSAGEWRITER_HPP +#define WEBFUSE_MESSAGEWRITER_HPP + +#include "webfuse/request_type.hpp" +#include "webfuse/response_type.hpp" + +#include +#include + +#include +#include +#include + +namespace webfuse +{ + +class messagewriter +{ + messagewriter(messagewriter const &) = delete; + messagewriter& operator=(messagewriter const &) = delete; +public: + explicit messagewriter(request_type req_type); + explicit messagewriter(response_type res_type); + ~messagewriter() = default; + messagewriter(messagewriter && other); + messagewriter& operator=(messagewriter && other); + + void set_id(uint32_t value); + uint32_t get_id() const; + + void write_bool(bool value); + void write_u8(uint8_t value); + void write_i8(int8_t value); + void write_i32(int32_t value); + void write_u32(uint32_t value); + void write_u64(uint64_t value); + void write_str(std::string const &value); + void write_data(char const * buffer, size_t size); + void write_strings(std::vector const & list); + + void write_attr(struct stat const * attr); + void write_access_mode(int value); + void write_rename_flags(unsigned int value); + void write_mode(mode_t value); + void write_uid(uid_t value); + void write_gid(gid_t value); + void write_openflags(int value); + void write_time(timespec const & value); + void write_statistics(struct statvfs const * statistics); + + unsigned char * get_data(size_t &size); + +private: + uint32_t id; + std::vector data; +}; + +} + +#endif diff --git a/src/webfuse/ws/server.cpp b/src/webfuse/ws/server.cpp new file mode 100644 index 0000000..e41eba8 --- /dev/null +++ b/src/webfuse/ws/server.cpp @@ -0,0 +1,189 @@ +#include "webfuse/ws/server.hpp" +#include "webfuse/ws/server_handler.hpp" + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + + +extern "C" +{ + +static int ws_server_callback(struct lws *wsi, enum lws_callback_reasons reason, + void *user, void *in, size_t len) +{ + auto const * protocol = lws_get_protocol(wsi); + if (nullptr == protocol) { return 0; } + if (&ws_server_callback != protocol->callback) { return 0; } + + auto * handler = reinterpret_cast(protocol->user); + + int result = 0; + switch(reason) + { + case LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION: + result = handler->filter_connection(wsi); + break; + case LWS_CALLBACK_ESTABLISHED: + result = handler->on_established(wsi); + break; + case LWS_CALLBACK_CLOSED: + handler->on_closed(wsi); + break; + case LWS_CALLBACK_RECEIVE: + handler->on_receive(wsi, in, len); + break; + case LWS_CALLBACK_SERVER_WRITEABLE: + result = handler->on_writable(); + break; + default: + break; + } + + return result; +} + + +} + +namespace webfuse +{ + +class ws_server::detail +{ + detail(detail const &) = delete; + detail& operator=(detail const &) = delete; + detail(detail &&) = delete; + detail& operator=(detail &&) = delete; +public: + detail(ws_config const & config) + : shutdown_requested(false) + , docroot(config.docroot) + , data(config.authenticator, config.auth_header) + , timeout_secs(config.timeout_secs) + { + lws_set_log_level(0, nullptr); + + memset(reinterpret_cast(&http_mount), 0, sizeof(http_mount)); + http_mount.mount_next = nullptr; + http_mount.mountpoint = "/"; + http_mount.origin = docroot.c_str(); + http_mount.def = "index.html"; + http_mount.origin_protocol = LWSMPRO_FILE; + http_mount.mountpoint_len = 1; + + memset(reinterpret_cast(protocols), 0, sizeof(protocols)); + protocols[0].name = "http"; + protocols[0].callback = lws_callback_http_dummy; + + protocols[1].name = "webfuse2"; + protocols[1].callback = &ws_server_callback; + protocols[1].per_session_data_size = 0; + protocols[1].user = reinterpret_cast(&data); + + memset(reinterpret_cast(&info), 0, sizeof(info)); + info.port = config.port; + info.mounts = &http_mount; + info.protocols = (docroot.empty()) ? &protocols[1] : protocols; + info.vhost_name = config.vhost_name.c_str(); + info.options = LWS_SERVER_OPTION_HTTP_HEADERS_SECURITY_BEST_PRACTICES_ENFORCE | LWS_SERVER_OPTION_EXPLICIT_VHOSTS; + + if (config.use_tls) + { + info.options |= LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT; + info.ssl_cert_filepath = config.cert_path.c_str(); + info.ssl_private_key_filepath = config.key_path.c_str(); + } + + context = lws_create_context(&info); + + lws_create_vhost(context, &info); + // lws_vhost * const vhost = lws_create_vhost(context, &info); + // int port = lws_get_vhost_port(vhost); + + thread = std::thread([this]() { + while (!shutdown_requested) + { + data.poll(); + lws_service(context, 0); + } + + }); + } + + ~detail() + { + shutdown_requested = true; + lws_cancel_service(context); + thread.join(); + lws_context_destroy(context); + } + + std::thread thread; + std::atomic shutdown_requested; + lws_protocols protocols[3]; + lws_http_mount http_mount; + lws_context_creation_info info; + lws_context * context; + std::string docroot; + server_handler data; + uint64_t timeout_secs; +}; + +ws_server::ws_server(ws_config const & config) +: d(new detail(config)) +{ + +} + +ws_server::~ws_server() +{ + delete d; +} + +ws_server::ws_server(ws_server && other) +{ + this->d = other.d; + other.d = nullptr; +} + +ws_server& ws_server::operator=(ws_server && other) +{ + if (this != &other) + { + delete this->d; + this->d = other.d; + other.d = nullptr; + } + + return *this; +} + +messagereader ws_server::perform(messagewriter writer) +{ + auto result = d->data.perform(std::move(writer)); + + lws_cancel_service(d->context); + if(std::future_status::timeout == result.wait_for(std::chrono::seconds(d->timeout_secs))) + { + throw std::runtime_error("timeout"); + } + + return std::move(result.get()); +} + + +} diff --git a/src/webfuse/ws/server.hpp b/src/webfuse/ws/server.hpp new file mode 100644 index 0000000..84766b2 --- /dev/null +++ b/src/webfuse/ws/server.hpp @@ -0,0 +1,33 @@ +#ifndef WEBFUSE_WSSERVER_HPP +#define WEBFUSE_WSSERVER_HPP + +#include "webfuse/ws/config.hpp" +#include "webfuse/ws/messagewriter.hpp" +#include "webfuse/ws/messagereader.hpp" + +#include +#include +#include + +namespace webfuse +{ + +class ws_server +{ + ws_server(ws_server const &) = delete; + ws_server& operator=(ws_server const &) = delete; +public: + ws_server(ws_config const & config); + ~ws_server(); + ws_server(ws_server && other); + ws_server& operator=(ws_server && other); + + messagereader perform(messagewriter writer); +private: + class detail; + detail * d; +}; + +} + +#endif diff --git a/src/webfuse/ws/server_handler.cpp b/src/webfuse/ws/server_handler.cpp new file mode 100644 index 0000000..6fb1208 --- /dev/null +++ b/src/webfuse/ws/server_handler.cpp @@ -0,0 +1,309 @@ +#include "webfuse/ws/server_handler.hpp" +#include "webfuse/util/authenticator.hpp" + +#include +#include +#include + + +namespace +{ +std::string get_auth_token_of_known_header(lws * wsi, lws_token_indexes header) +{ + std::string token; + int const length = lws_hdr_total_length(wsi, header); + if (length > 0) + { + std::vector data(length + 1); + int const actual_length = lws_hdr_copy(wsi, data.data(), length + 1, header); + if (actual_length > 0) + { + token = data.data(); + } + } + + return token; +} + +std::string get_auth_token_from_custom_header(lws * wsi, std::string const & auth_header) +{ + std::string token; + int const length = lws_hdr_custom_length(wsi, auth_header.c_str(), auth_header.size()); + if (length > 0) + { + std::vector data(length + 1); + int const actual_length = lws_hdr_custom_copy(wsi, data.data(), length + 1, + auth_header.c_str(), auth_header.size()); + if (actual_length > 0) + { + token = data.data(); + } + } + + return token; +} + +} + +namespace webfuse +{ + +server_handler::server_handler(std::string const & auth_app, std::string const & auth_hdr) +: connection(nullptr) +, id(0) +, shutdown_requested(false) +, is_authenticated(false) +, authenticator(auth_app) +, auth_header(auth_hdr) +{ + +} + + +int server_handler::filter_connection(lws * wsi) +{ + return authenticate_via_header(wsi); +} + +int server_handler::on_established(lws * wsi) +{ + if (nullptr == connection) + { + connection = wsi; + id = 0; + + if ((!is_authenticated) && (!authenticator.empty())) + { + { + messagewriter writer(request_type::getcreds); + std::lock_guard lock(mut); + + uint32_t id = next_id(); + writer.set_id(id); + requests.emplace(std::move(writer)); + } + + lws_callback_on_writable(wsi); + } + + return 0; + } + + // already connected: refuse + return -1; +} + + +void server_handler::on_receive(lws * wsi, void* in, int len) +{ + auto * fragment = reinterpret_cast(in); + current_message.append(fragment, len); + if (0 != lws_is_final_fragment(wsi)) + { + try + { + webfuse::messagereader reader(current_message); + uint32_t id = reader.read_u32(); + auto const message_type = reader.read_u8(); // read message type: ToDo: use it + + if (static_cast(message_type) == response_type::getcreds) + { + finish_authentication(wsi, std::move(reader)); + } + else + { + std::lock_guard lock(mut); + auto it = pending_responses.find(id); + if (it != pending_responses.end()) + { + it->second.set_value(std::move(reader)); + pending_responses.erase(it); + } + else + { + // ToDo: log request not found + std::cout << "warning: request not found: id=" << id << std::endl; + for(auto const & entry: pending_responses) + { + std::cout << "\t" << entry.first << std::endl; + } + } + } + } + catch(...) + { + // ToDo: log invalid message + std::cout << "warning: invalid message" << std::endl; + } + } +} + +int server_handler::on_writable() +{ + if (shutdown_requested) { return -1; } + + webfuse::messagewriter writer(webfuse::request_type::unknown); + bool has_msg = false; + bool has_more = false; + + { + std::lock_guard lock(mut); + has_msg = !(requests.empty()); + if (has_msg) + { + has_msg = true; + writer = std::move(requests.front()); + requests.pop(); + + has_more = !(requests.empty()); + } + } + + if (has_msg) + { + size_t size; + unsigned char * raw_data = writer.get_data(size); + lws_write(connection, raw_data, size, LWS_WRITE_BINARY); + } + + if (has_more) + { + lws_callback_on_writable(connection); + } + + return 0; +} + +void server_handler::on_closed(lws * wsi) +{ + if (wsi == connection) + { + connection = nullptr; + is_authenticated = false; + shutdown_requested = false; + } +} + +void server_handler::poll() +{ + std::lock_guard lock(mut); + if (!requests.empty()) + { + if (nullptr != connection) + { + lws_callback_on_writable(connection); + } + else + { + requests = std::move(std::queue()); + pending_responses.clear(); + } + } +} + + +std::future server_handler::perform(messagewriter writer) +{ + std::promise p; + std::future result = p.get_future(); + if (is_authenticated) + { + std::lock_guard lock(mut); + uint32_t id = next_id(); + writer.set_id(id); + requests.emplace(std::move(writer)); + pending_responses.emplace(id, std::move(p)); + } + else + { + try + { + throw std::runtime_error("unauthenticated"); + } + catch(std::exception const &ex) + { + p.set_exception(std::current_exception()); + } + + } + + return result; + +} + + +int server_handler::authenticate_via_header(lws * wsi) +{ + // authentication is disabled + if (authenticator.empty()) + { + is_authenticated = true; + return 0; + } + + // authentication is enabled, but not via HTTP header + if (auth_header.empty()) + { + is_authenticated = false; + return 0; + } + + // delay authentication if HTTP header is not provided + std::string token = get_auth_token(wsi); + if (token.empty()) + { + is_authenticated = false; + return 0; + } + + // close connection, when authentication fails + webfuse::authenticator auth(authenticator); + int const result = auth.authenticate(token) ? 0 : -1; + is_authenticated = (result == 0); + return result; +} + +std::string server_handler::get_auth_token(lws * wsi) const +{ + if (auth_header == "authorization") + { + return get_auth_token_of_known_header(wsi, WSI_TOKEN_HTTP_AUTHORIZATION); + } + + if (auth_header == "x-auth-token") + { + return get_auth_token_of_known_header(wsi, WSI_TOKEN_X_AUTH_TOKEN); + } + + return get_auth_token_from_custom_header(wsi, auth_header); +} + +uint32_t server_handler::next_id() +{ + id++; + if (0 == id) + { + id = 1; + } + return id; +} + +void server_handler::finish_authentication(lws * wsi, messagereader reader) +{ + auto const credentials = reader.read_str(); + + webfuse::authenticator auth(authenticator); + auto const result = auth.authenticate(credentials); + if (result) + { + is_authenticated = true; + } + else + { + shutdown_requested = true; + lws_callback_on_writable(wsi); + } +} + + +} diff --git a/src/webfuse/ws/server_handler.hpp b/src/webfuse/ws/server_handler.hpp new file mode 100644 index 0000000..025a6ed --- /dev/null +++ b/src/webfuse/ws/server_handler.hpp @@ -0,0 +1,61 @@ +#ifndef WEBFUSE_SERVER_HANDLER_HPP +#define WEBFUSE_SERVER_HANDLER_HPP + +#include "webfuse/ws/messagereader.hpp" +#include "webfuse/ws/messagewriter.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +namespace webfuse +{ + +class server_handler +{ +public: + server_handler(std::string const & auth_app, std::string const & auth_hdr); + + int filter_connection(lws * wsi); + + int on_established(lws* wsi); + void on_receive(lws * wsi, void* in, int len); + int on_writable(); + void on_closed(lws * wsi); + + std::future perform(messagewriter writer); + void poll(); + +private: + int authenticate_via_header(lws * wsi); + std::string get_auth_token(lws * wsi) const; + uint32_t next_id(); + void finish_authentication(lws * wsi, messagereader reader); + + struct lws * connection; + uint32_t id; + bool shutdown_requested; + + std::atomic is_authenticated; + std::string authenticator; + std::string auth_header; + + std::string current_message; + + std::mutex mut; + std::queue requests; + std::unordered_map> pending_responses; + + + +}; + + +} + +#endif diff --git a/src/webfuse/ws/url.cpp b/src/webfuse/ws/url.cpp new file mode 100644 index 0000000..6209564 --- /dev/null +++ b/src/webfuse/ws/url.cpp @@ -0,0 +1,70 @@ +#include "webfuse/ws/url.hpp" + +#include +#include + +namespace +{ + +bool starts_with(std::string const & value, std::string const & prefix) +{ + return (0 == value.find(prefix)); +} + +std::string parse_protocol(std::string const &url, bool &use_tls) +{ + if (starts_with(url, "ws://")) + { + use_tls = false; + return url.substr(strlen("ws://")); + } + + if (starts_with(url, "wss://")) + { + use_tls = true; + return url.substr(strlen("wss://")); + } + + throw std::runtime_error("unknown protocol"); +} + + +} + +namespace webfuse +{ + +constexpr uint16_t const ws_port = 80; +constexpr uint16_t const wss_port = 443; + +ws_url::ws_url(std::string const & url) +{ + auto remainder = parse_protocol(url, use_tls); + + auto const path_start = remainder.find('/'); + if (path_start != std::string::npos) + { + path = remainder.substr(path_start); + remainder = remainder.substr(0, path_start); + } + else + { + path = "/"; + } + + auto const port_start = remainder.find(':'); + if (port_start != std::string::npos) + { + auto const port_str = remainder.substr(port_start + 1); + port = static_cast(std::stoi(port_str)); + hostname = remainder.substr(0, port_start); + } + else + { + port = (use_tls) ? wss_port : ws_port; + hostname = remainder; + } +} + + +} diff --git a/src/webfuse/ws/url.hpp b/src/webfuse/ws/url.hpp new file mode 100644 index 0000000..d7b07c7 --- /dev/null +++ b/src/webfuse/ws/url.hpp @@ -0,0 +1,24 @@ +#ifndef WEBFUSE_URL_HPP +#define WEBFUSE_URL_HPP + +#include +#include + +namespace webfuse +{ + +class ws_url +{ +public: + ws_url(std::string const & url); + ~ws_url() = default; + + bool use_tls; + std::string hostname; + uint16_t port; + std::string path; +}; + +} + +#endif diff --git a/test-src/integration/test_access.cpp b/test-src/integration/test_access.cpp new file mode 100644 index 0000000..6486c01 --- /dev/null +++ b/test-src/integration/test_access.cpp @@ -0,0 +1,71 @@ +#include "webfuse/webfuse.hpp" +#include "webfuse/test/fixture.hpp" +#include "webfuse/test/filesystem_mock.hpp" +#include "webfuse/test/daemon.hpp" + +#include +#include + +using testing::_; +using testing::Return; +using testing::Invoke; +using testing::AnyNumber; + +namespace +{ +int fs_getattr (std::string const & path, struct stat * attr) +{ + memset(reinterpret_cast(attr),0, sizeof(struct stat)); + + if (path == "/") + { + attr->st_nlink = 0; + attr->st_mode = S_IFDIR | 0755; + return 0; + } + else if (path == "/foo") + { + attr->st_nlink = 0; + attr->st_mode = S_IFREG | 0755; + return 0; + } + else + { + return -ENOENT; + } +} +} + +TEST(readdir, existing_file) +{ + webfuse::filesystem_mock fs; + EXPECT_CALL(fs, access("/",_)).Times(AnyNumber()).WillRepeatedly(Return(0)); + EXPECT_CALL(fs, access("/foo", F_OK)).WillOnce(Return(0)); + EXPECT_CALL(fs, access("/foo", R_OK)).WillOnce(Return(0)); + EXPECT_CALL(fs, access("/foo", W_OK)).WillOnce(Return(0)); + EXPECT_CALL(fs, access("/foo", X_OK)).WillOnce(Return(-EACCES)); + EXPECT_CALL(fs, getattr(_,_)).WillRepeatedly(Invoke(fs_getattr)); + + webfuse::fixture fixture(fs); + auto const path = fixture.get_path() + "/foo"; + + ASSERT_EQ(0, ::access(path.c_str(), F_OK)); + ASSERT_EQ(0, ::access(path.c_str(), R_OK)); + ASSERT_EQ(0, ::access(path.c_str(), W_OK)); + ASSERT_EQ(-1, ::access(path.c_str(), X_OK)); + ASSERT_EQ(EACCES, errno); +} + +TEST(access, non_existing_file) +{ + webfuse::filesystem_mock fs; + EXPECT_CALL(fs, access("/",_)).Times(AnyNumber()).WillRepeatedly(Return(0)); + EXPECT_CALL(fs, access("/foo", F_OK)).WillOnce(Return(-ENOENT)); + EXPECT_CALL(fs, getattr(_,_)).WillRepeatedly(Invoke(fs_getattr)); + + webfuse::fixture fixture(fs); + auto const path = fixture.get_path() + "/foo"; + + ASSERT_EQ(-1, ::access(path.c_str(), F_OK)); + ASSERT_EQ(ENOENT, errno); +} diff --git a/test-src/integration/test_chmod.cpp b/test-src/integration/test_chmod.cpp new file mode 100644 index 0000000..bff1fe9 --- /dev/null +++ b/test-src/integration/test_chmod.cpp @@ -0,0 +1,79 @@ +#include "webfuse/webfuse.hpp" +#include "webfuse/test/fixture.hpp" +#include "webfuse/test/filesystem_mock.hpp" + +#include + +using testing::_; +using testing::Return; +using testing::Invoke; +using testing::AnyNumber; + +TEST(chmod, success) +{ + bool link_created = false; + + webfuse::filesystem_mock fs; + EXPECT_CALL(fs, access("/",_)).Times(AnyNumber()).WillRepeatedly(Return(0)); + EXPECT_CALL(fs, getattr(_,_)).WillRepeatedly(Invoke([&link_created](std::string const & path, struct stat * attr){ + memset(reinterpret_cast(attr),0, sizeof(struct stat)); + + if (path == "/") + { + attr->st_nlink = 1; + attr->st_mode = S_IFDIR | 0755; + return 0; + } + if (path == "/some_file") + { + attr->st_nlink = 1; + attr->st_mode = S_IFREG | 0755; + return 0; + } + else + { + return -ENOENT; + } + })); + EXPECT_CALL(fs, chmod("/some_file", S_IFREG | 0644)).WillOnce(Return(0)); + + webfuse::fixture fixture(fs); + auto const path = fixture.get_path() + "/some_file"; + + ASSERT_EQ(0, ::chmod(path.c_str(), 0644)); +} + +TEST(chmod, fail) +{ + bool link_created = false; + + webfuse::filesystem_mock fs; + EXPECT_CALL(fs, access("/",_)).Times(AnyNumber()).WillRepeatedly(Return(0)); + EXPECT_CALL(fs, getattr(_,_)).WillRepeatedly(Invoke([&link_created](std::string const & path, struct stat * attr){ + memset(reinterpret_cast(attr),0, sizeof(struct stat)); + + if (path == "/") + { + attr->st_nlink = 1; + attr->st_mode = S_IFDIR | 0755; + return 0; + } + if (path == "/some_file") + { + attr->st_nlink = 1; + attr->st_mode = S_IFREG | 0755; + return 0; + } + else + { + return -ENOENT; + } + })); + EXPECT_CALL(fs, chmod("/some_file", S_IFREG | 0644)).WillOnce(Return(-EACCES)); + + webfuse::fixture fixture(fs); + auto const path = fixture.get_path() + "/some_file"; + + ASSERT_NE(0, ::chmod(path.c_str(), 0644)); + ASSERT_EQ(EACCES, errno); +} diff --git a/test-src/integration/test_chown.cpp b/test-src/integration/test_chown.cpp new file mode 100644 index 0000000..74407d7 --- /dev/null +++ b/test-src/integration/test_chown.cpp @@ -0,0 +1,79 @@ +#include "webfuse/webfuse.hpp" +#include "webfuse/test/fixture.hpp" +#include "webfuse/test/filesystem_mock.hpp" + +#include + +using testing::_; +using testing::Return; +using testing::Invoke; +using testing::AnyNumber; + +TEST(chown, success) +{ + bool link_created = false; + + webfuse::filesystem_mock fs; + EXPECT_CALL(fs, access("/",_)).Times(AnyNumber()).WillRepeatedly(Return(0)); + EXPECT_CALL(fs, getattr(_,_)).WillRepeatedly(Invoke([&link_created](std::string const & path, struct stat * attr){ + memset(reinterpret_cast(attr),0, sizeof(struct stat)); + + if (path == "/") + { + attr->st_nlink = 1; + attr->st_mode = S_IFDIR | 0755; + return 0; + } + if (path == "/some_file") + { + attr->st_nlink = 1; + attr->st_mode = S_IFREG | 0755; + return 0; + } + else + { + return -ENOENT; + } + })); + EXPECT_CALL(fs, chown("/some_file", 42, 42)).WillOnce(Return(0)); + + webfuse::fixture fixture(fs); + auto const path = fixture.get_path() + "/some_file"; + + ASSERT_EQ(0, ::chown(path.c_str(), 42, 42)); +} + +TEST(chown, fail) +{ + bool link_created = false; + + webfuse::filesystem_mock fs; + EXPECT_CALL(fs, access("/",_)).Times(AnyNumber()).WillRepeatedly(Return(0)); + EXPECT_CALL(fs, getattr(_,_)).WillRepeatedly(Invoke([&link_created](std::string const & path, struct stat * attr){ + memset(reinterpret_cast(attr),0, sizeof(struct stat)); + + if (path == "/") + { + attr->st_nlink = 1; + attr->st_mode = S_IFDIR | 0755; + return 0; + } + if (path == "/some_file") + { + attr->st_nlink = 1; + attr->st_mode = S_IFREG | 0755; + return 0; + } + else + { + return -ENOENT; + } + })); + EXPECT_CALL(fs, chown("/some_file", 42, 42)).WillOnce(Return(-EACCES)); + + webfuse::fixture fixture(fs); + auto const path = fixture.get_path() + "/some_file"; + + ASSERT_NE(0, ::chown(path.c_str(), 42, 42)); + ASSERT_EQ(EACCES, errno); +} diff --git a/test-src/integration/test_fsync.cpp b/test-src/integration/test_fsync.cpp new file mode 100644 index 0000000..9117f52 --- /dev/null +++ b/test-src/integration/test_fsync.cpp @@ -0,0 +1,56 @@ +#include "webfuse/webfuse.hpp" +#include "webfuse/test/fixture.hpp" +#include "webfuse/test/filesystem_mock.hpp" + +#include + +#include + +using testing::_; +using testing::Return; +using testing::Invoke; +using testing::AnyNumber; +using testing::AtMost; + +TEST(fsync, success) +{ + bool file_created = false; + + webfuse::filesystem_mock fs; + EXPECT_CALL(fs, access("/",_)).Times(AnyNumber()).WillRepeatedly(Return(0)); + EXPECT_CALL(fs, getattr(_,_)).WillRepeatedly(Invoke([&file_created](std::string const & path, struct stat * attr){ + memset(reinterpret_cast(attr),0, sizeof(struct stat)); + + if (path == "/") + { + attr->st_nlink = 1; + attr->st_mode = S_IFDIR | 0755; + return 0; + } + if ((path == "/some_file") && (file_created)) + { + attr->st_nlink = 1; + attr->st_mode = S_IFREG | 0644; + attr->st_size = 0; + return 0; + } + else + { + return -ENOENT; + } + })); + EXPECT_CALL(fs, create("/some_file", _, _)).WillOnce(Invoke([&file_created](auto const &, auto const, auto &){ + file_created = true; + return 0; + })); + EXPECT_CALL(fs, fsync("/some_file", _, _)).WillOnce(Return(0)); + EXPECT_CALL(fs, release("/some_file", _)).Times(AtMost(1)).WillOnce(Return(0)); + + webfuse::fixture fixture(fs); + auto const path = fixture.get_path() + "/some_file"; + + int fd = creat(path.c_str(), 0644); + ASSERT_LT(0, fd); + ASSERT_EQ(0, ::fsync(fd)); + close(fd); +} diff --git a/test-src/integration/test_link.cpp b/test-src/integration/test_link.cpp new file mode 100644 index 0000000..5e1f490 --- /dev/null +++ b/test-src/integration/test_link.cpp @@ -0,0 +1,88 @@ +#include "webfuse/webfuse.hpp" +#include "webfuse/test/fixture.hpp" +#include "webfuse/test/filesystem_mock.hpp" + +#include + +using testing::_; +using testing::Return; +using testing::Invoke; +using testing::AnyNumber; + +TEST(link, sucessfully_create_link) +{ + bool link_created = false; + + webfuse::filesystem_mock fs; + EXPECT_CALL(fs, access("/",_)).Times(AnyNumber()).WillRepeatedly(Return(0)); + EXPECT_CALL(fs, getattr(_,_)).WillRepeatedly(Invoke([&link_created](std::string const & path, struct stat * attr){ + memset(reinterpret_cast(attr),0, sizeof(struct stat)); + + if (path == "/") + { + attr->st_nlink = 1; + attr->st_mode = S_IFDIR | 0755; + return 0; + } + else if (path == "/link-target") + { + attr->st_nlink = 1; + attr->st_mode = S_IFREG | 0755; + return 0; + } + else if ((path == "/some_link") && (link_created)) + { + attr->st_nlink = 2; + attr->st_mode = S_IFREG | 0755; + return 0; + } + else + { + return -ENOENT; + } + })); + EXPECT_CALL(fs, link("/link-target", "/some_link")).WillOnce(Invoke([&link_created](auto const &, auto const &){ + link_created = true; + return 0; + })); + + webfuse::fixture fixture(fs); + auto const path = fixture.get_path() + "/some_link"; + auto const from = fixture.get_path() + "/link-target"; + + ASSERT_EQ(0, ::link(from.c_str(), path.c_str())); +} + +TEST(link, failed_to_create_link) +{ + webfuse::filesystem_mock fs; + EXPECT_CALL(fs, access("/",_)).Times(AnyNumber()).WillRepeatedly(Return(0)); + EXPECT_CALL(fs, getattr(_,_)).WillRepeatedly(Invoke([](std::string const & path, struct stat * attr){ + memset(reinterpret_cast(attr),0, sizeof(struct stat)); + + if (path == "/") + { + attr->st_nlink = 1; + attr->st_mode = S_IFDIR | 0755; + return 0; + } + else if (path == "/link-target") + { + attr->st_nlink = 1; + attr->st_mode = S_IFREG | 0755; + return 0; + } + else + { + return -ENOENT; + } + })); + EXPECT_CALL(fs, link("/link-target", "/some_link")).WillOnce(Return(-EDQUOT)); + + webfuse::fixture fixture(fs); + auto const path = fixture.get_path() + "/some_link"; + auto const from = fixture.get_path() + "/link-target"; + + ASSERT_NE(0, ::link(from.c_str(), path.c_str())); + ASSERT_EQ(EDQUOT, errno); +} diff --git a/test-src/integration/test_mkdir.cpp b/test-src/integration/test_mkdir.cpp new file mode 100644 index 0000000..7cce68a --- /dev/null +++ b/test-src/integration/test_mkdir.cpp @@ -0,0 +1,51 @@ +#include "webfuse/webfuse.hpp" +#include "webfuse/test/fixture.hpp" +#include "webfuse/test/filesystem_mock.hpp" + +#include +#include +#include + + +using testing::_; +using testing::Return; +using testing::Invoke; +using testing::AnyNumber; +using testing::AtMost; + +TEST(mkdir, success) +{ + bool created = false; + + webfuse::filesystem_mock fs; + EXPECT_CALL(fs, access("/",_)).Times(AnyNumber()).WillRepeatedly(Return(0)); + EXPECT_CALL(fs, getattr(_,_)).WillRepeatedly(Invoke([&created](std::string const & path, struct stat * attr){ + memset(reinterpret_cast(attr),0, sizeof(struct stat)); + + if (path == "/") + { + attr->st_nlink = 1; + attr->st_mode = S_IFDIR | 0755; + return 0; + } + if ((path == "/some_dir") && (created)) + { + attr->st_nlink = 1; + attr->st_mode = S_IFDIR | 0644; + return 0; + } + else + { + return -ENOENT; + } + })); + EXPECT_CALL(fs, mkdir("/some_dir", _)).WillOnce(Invoke([&created](auto const &, auto) { + created = true; + return 0; + })); + + webfuse::fixture fixture(fs); + auto const path = fixture.get_path() + "/some_dir"; + + ASSERT_EQ(0, ::mkdir(path.c_str(), 0644)); +} diff --git a/test-src/integration/test_mknod.cpp b/test-src/integration/test_mknod.cpp new file mode 100644 index 0000000..2af4234 --- /dev/null +++ b/test-src/integration/test_mknod.cpp @@ -0,0 +1,52 @@ +#include "webfuse/webfuse.hpp" +#include "webfuse/test/fixture.hpp" +#include "webfuse/test/filesystem_mock.hpp" + +#include +#include +#include + +using testing::_; +using testing::Return; +using testing::Invoke; +using testing::AnyNumber; +using testing::AtMost; + +TEST(mknod, success) +{ + bool file_created = false; + + webfuse::filesystem_mock fs; + EXPECT_CALL(fs, access("/",_)).Times(AnyNumber()).WillRepeatedly(Return(0)); + EXPECT_CALL(fs, getattr(_,_)).WillRepeatedly(Invoke([&file_created](std::string const & path, struct stat * attr){ + memset(reinterpret_cast(attr),0, sizeof(struct stat)); + + if (path == "/") + { + attr->st_nlink = 1; + attr->st_mode = S_IFDIR | 0755; + return 0; + } + else if ((path == "/some_dev") && (file_created)) + { + attr->st_nlink = 1; + attr->st_mode = S_IFIFO | 0644; + attr->st_size = 0; + return 0; + } + else + { + return -ENOENT; + } + })); + EXPECT_CALL(fs, mknod("/some_dev", _, _)).Times(AtMost(1)).WillOnce(Invoke([&file_created](auto const &, auto, auto ){ + file_created = true; + return 0; + })); + EXPECT_CALL(fs, release("/some_dev", _)).Times(AtMost(1)).WillOnce(Return(0)); + + webfuse::fixture fixture(fs); + auto const path = fixture.get_path() + "/some_dev"; + + ASSERT_EQ(0, ::mkfifo(path.c_str(), 0644)); +} diff --git a/test-src/integration/test_open.cpp b/test-src/integration/test_open.cpp new file mode 100644 index 0000000..dcfb3e9 --- /dev/null +++ b/test-src/integration/test_open.cpp @@ -0,0 +1,49 @@ +#include "webfuse/webfuse.hpp" +#include "webfuse/test/fixture.hpp" +#include "webfuse/test/filesystem_mock.hpp" + +#include +#include + +using testing::_; +using testing::Return; +using testing::Invoke; +using testing::AnyNumber; +using testing::AtMost; + +TEST(open, success) +{ + bool link_created = false; + + webfuse::filesystem_mock fs; + EXPECT_CALL(fs, access("/",_)).Times(AnyNumber()).WillRepeatedly(Return(0)); + EXPECT_CALL(fs, getattr(_,_)).WillRepeatedly(Invoke([&link_created](std::string const & path, struct stat * attr){ + memset(reinterpret_cast(attr),0, sizeof(struct stat)); + + if (path == "/") + { + attr->st_nlink = 1; + attr->st_mode = S_IFDIR | 0755; + return 0; + } + if (path == "/some_file") + { + attr->st_nlink = 1; + attr->st_mode = S_IFREG | 0755; + return 0; + } + else + { + return -ENOENT; + } + })); + EXPECT_CALL(fs, open("/some_file", O_RDONLY, _)).WillOnce(Return(0)); + EXPECT_CALL(fs, release("/some_file", _)).Times(AtMost(1)).WillOnce(Return(0)); + + webfuse::fixture fixture(fs); + auto const path = fixture.get_path() + "/some_file"; + + int fd = ::open(path.c_str(), O_RDONLY); + ASSERT_LT(0, fd); + ASSERT_EQ(0, ::close(fd)); +} diff --git a/test-src/integration/test_read.cpp b/test-src/integration/test_read.cpp new file mode 100644 index 0000000..eee59da --- /dev/null +++ b/test-src/integration/test_read.cpp @@ -0,0 +1,70 @@ +#include "webfuse/webfuse.hpp" +#include "webfuse/test/fixture.hpp" +#include "webfuse/test/filesystem_mock.hpp" + +#include +#include +#include + + +using testing::_; +using testing::Return; +using testing::Invoke; +using testing::AnyNumber; +using testing::AtMost; + +TEST(read, success) +{ + std::string expected = "Hello, World!"; + + webfuse::filesystem_mock fs; + EXPECT_CALL(fs, access("/",_)).Times(AnyNumber()).WillRepeatedly(Return(0)); + EXPECT_CALL(fs, getattr(_,_)).WillRepeatedly(Invoke([&expected](std::string const & path, struct stat * attr){ + memset(reinterpret_cast(attr),0, sizeof(struct stat)); + + if (path == "/") + { + attr->st_nlink = 1; + attr->st_mode = S_IFDIR | 0755; + return 0; + } + if (path == "/some_file") + { + attr->st_nlink = 1; + attr->st_mode = S_IFREG | 0644; + attr->st_size = expected.size(); + return 0; + } + else + { + return -ENOENT; + } + })); + EXPECT_CALL(fs, open("/some_file", _, _)).WillOnce(Return(0)); + EXPECT_CALL(fs, read("/some_file", _, _, _, _)).Times(AnyNumber()).WillRepeatedly(Invoke([&expected](auto const &, char * buffer, size_t buffer_size, uint64_t offset, auto){ + if (offset < expected.size()) + { + auto const count = std::min(buffer_size, expected.size() - offset); + memcpy(reinterpret_cast(buffer), reinterpret_cast(&expected[offset]), count); + return (int) count; + } + else + { + return 0; + } + })); + EXPECT_CALL(fs, release("/some_file", _)).Times(AtMost(1)).WillOnce(Return(0)); + + webfuse::fixture fixture(fs); + auto const path = fixture.get_path() + "/some_file"; + + std::string contents; + { + std::ifstream in(path); + std::stringstream buffer; + buffer << in.rdbuf(); + contents = buffer.str(); + } + + ASSERT_EQ(expected, contents); +} diff --git a/test-src/integration/test_readdir.cpp b/test-src/integration/test_readdir.cpp new file mode 100644 index 0000000..7ea787f --- /dev/null +++ b/test-src/integration/test_readdir.cpp @@ -0,0 +1,97 @@ +#include "webfuse/webfuse.hpp" +#include "webfuse/test/fixture.hpp" +#include "webfuse/test/filesystem_mock.hpp" +#include "webfuse/test/daemon.hpp" + +#include +#include +#include + +#include +#include + +using testing::_; +using testing::Return; +using testing::Invoke; +using testing::AnyNumber; + +TEST(readdir, existing_dir) +{ + webfuse::filesystem_mock fs; + EXPECT_CALL(fs, access("/",_)).Times(AnyNumber()).WillRepeatedly(Return(0)); + EXPECT_CALL(fs, getattr(_,_)).WillRepeatedly(Invoke([](auto const & path, auto * attr){ + memset(reinterpret_cast(attr),0, sizeof(struct stat)); + + if ((path == "/") or (path == "/some_dir")) + { + attr->st_nlink = 1; + attr->st_mode = S_IFDIR | 0755; + return 0; + } + else + { + return -ENOENT; + } + })); + EXPECT_CALL(fs, readdir("/some_dir",_)).WillOnce(Invoke([](auto const & path, auto & entries) { + entries.push_back("foo"); + return 0; + })); + + webfuse::fixture fixture(fs); + auto const path = fixture.get_path() + "/some_dir"; + + std::unordered_map expected_entries = { + {".", false}, + {"..", false}, + {"foo", false}, + }; + + DIR * dir = opendir(path.c_str()); + ASSERT_NE(nullptr, dir); + dirent * entry = readdir(dir); + int count = 0; + while (nullptr != entry) + { + count++; + + auto it = expected_entries.find(entry->d_name); + ASSERT_NE(expected_entries.end(), it); + ASSERT_FALSE(it->second); + it->second = true; + + entry = readdir(dir); + } + closedir(dir); + + ASSERT_EQ(3, count); +} + +TEST(readdir, non_existing_dir) +{ + webfuse::filesystem_mock fs; + EXPECT_CALL(fs, access("/",_)).Times(AnyNumber()).WillRepeatedly(Return(0)); + EXPECT_CALL(fs, getattr(_,_)).WillRepeatedly(Invoke([](auto const & path, auto * attr){ + memset(reinterpret_cast(attr),0, sizeof(struct stat)); + + if (path == "/") + { + attr->st_nlink = 1; + attr->st_mode = S_IFDIR | 0755; + return 0; + } + else + { + return -ENOENT; + } + })); + EXPECT_CALL(fs, readdir("/some_dir",_)).Times(0); + + webfuse::fixture fixture(fs); + auto const path = fixture.get_path() + "/some_dir"; + + DIR * dir = opendir(path.c_str()); + ASSERT_EQ(nullptr, dir); + ASSERT_EQ(ENOENT, errno); +} + diff --git a/test-src/integration/test_readlink.cpp b/test-src/integration/test_readlink.cpp new file mode 100644 index 0000000..ce0bf6b --- /dev/null +++ b/test-src/integration/test_readlink.cpp @@ -0,0 +1,74 @@ +#include "webfuse/webfuse.hpp" +#include "webfuse/test/fixture.hpp" +#include "webfuse/test/filesystem_mock.hpp" + +#include + + +using testing::_; +using testing::Return; +using testing::Invoke; +using testing::AnyNumber; + +namespace +{ + +int fs_getattr(std::string const & path, struct stat * attr){ + memset(reinterpret_cast(attr),0, sizeof(struct stat)); + + if (path == "/") + { + attr->st_nlink = 1; + attr->st_mode = S_IFDIR | 0755; + return 0; + } + else if (path == "/some_link") + { + attr->st_nlink = 1; + attr->st_mode = S_IFLNK | 0755; + return 0; + } + else + { + return -ENOENT; + } + } + +} + +TEST(readlink, existing_link) +{ + webfuse::filesystem_mock fs; + EXPECT_CALL(fs, access("/",_)).Times(AnyNumber()).WillRepeatedly(Return(0)); + EXPECT_CALL(fs, getattr(_,_)).WillRepeatedly(Invoke(fs_getattr)); + EXPECT_CALL(fs, readlink("/some_link",_)).WillOnce(Invoke([](auto const & path, auto & out) { + out = "link-target"; + return 0; + })); + + webfuse::fixture fixture(fs); + auto const path = fixture.get_path() + "/some_link"; + + char buffer[100]; + auto const length = ::readlink(path.c_str(), buffer, 99); + ASSERT_LT(0, length); + buffer[length] = '\0'; + ASSERT_STREQ("link-target", buffer); +} + +TEST(readlink, non_existing_link) +{ + webfuse::filesystem_mock fs; + EXPECT_CALL(fs, access("/",_)).Times(AnyNumber()).WillRepeatedly(Return(0)); + EXPECT_CALL(fs, getattr(_,_)).WillRepeatedly(Invoke(fs_getattr)); + EXPECT_CALL(fs, readlink("/some_link",_)).WillOnce(Invoke([](auto const & path, auto & out) { + return -ENOENT; + })); + + webfuse::fixture fixture(fs); + auto const path = fixture.get_path() + "/some_link"; + + char buffer[100]; + ASSERT_GT(0, ::readlink(path.c_str(), buffer, 100)); + ASSERT_EQ(ENOENT, errno); +} diff --git a/test-src/integration/test_rename.cpp b/test-src/integration/test_rename.cpp new file mode 100644 index 0000000..d944c99 --- /dev/null +++ b/test-src/integration/test_rename.cpp @@ -0,0 +1,81 @@ +#include "webfuse/webfuse.hpp" +#include "webfuse/test/fixture.hpp" +#include "webfuse/test/filesystem_mock.hpp" + +#include + +using testing::_; +using testing::Return; +using testing::Invoke; +using testing::AnyNumber; + +TEST(rename, success) +{ + bool link_created = false; + + webfuse::filesystem_mock fs; + EXPECT_CALL(fs, access("/",_)).Times(AnyNumber()).WillRepeatedly(Return(0)); + EXPECT_CALL(fs, getattr(_,_)).WillRepeatedly(Invoke([&link_created](std::string const & path, struct stat * attr){ + memset(reinterpret_cast(attr),0, sizeof(struct stat)); + + if (path == "/") + { + attr->st_nlink = 1; + attr->st_mode = S_IFDIR | 0755; + return 0; + } + if (path == "/some_file") + { + attr->st_nlink = 1; + attr->st_mode = S_IFREG | 0755; + return 0; + } + else + { + return -ENOENT; + } + })); + EXPECT_CALL(fs, rename("/some_file", "/another_file", _)).WillOnce(Return(0)); + + webfuse::fixture fixture(fs); + auto const old_name = fixture.get_path() + "/some_file"; + auto const new_name = fixture.get_path() + "/another_file"; + + ASSERT_EQ(0, ::rename(old_name.c_str(), new_name.c_str())); +} + +TEST(rename, fail) +{ + bool link_created = false; + + webfuse::filesystem_mock fs; + EXPECT_CALL(fs, access("/",_)).Times(AnyNumber()).WillRepeatedly(Return(0)); + EXPECT_CALL(fs, getattr(_,_)).WillRepeatedly(Invoke([&link_created](std::string const & path, struct stat * attr){ + memset(reinterpret_cast(attr),0, sizeof(struct stat)); + + if (path == "/") + { + attr->st_nlink = 1; + attr->st_mode = S_IFDIR | 0755; + return 0; + } + if (path == "/some_file") + { + attr->st_nlink = 1; + attr->st_mode = S_IFREG | 0755; + return 0; + } + else + { + return -ENOENT; + } + })); + EXPECT_CALL(fs, rename("/some_file", "/another_file", _)).WillOnce(Return(-EDQUOT)); + + webfuse::fixture fixture(fs); + auto const old_name = fixture.get_path() + "/some_file"; + auto const new_name = fixture.get_path() + "/another_file"; + + ASSERT_NE(0, ::rename(old_name.c_str(), new_name.c_str())); + ASSERT_EQ(EDQUOT, errno); +} diff --git a/test-src/integration/test_rmdir.cpp b/test-src/integration/test_rmdir.cpp new file mode 100644 index 0000000..1df29cc --- /dev/null +++ b/test-src/integration/test_rmdir.cpp @@ -0,0 +1,51 @@ +#include "webfuse/webfuse.hpp" +#include "webfuse/test/fixture.hpp" +#include "webfuse/test/filesystem_mock.hpp" + +#include +#include +#include + + +using testing::_; +using testing::Return; +using testing::Invoke; +using testing::AnyNumber; +using testing::AtMost; + +TEST(rmdir, success) +{ + bool removed = false; + + webfuse::filesystem_mock fs; + EXPECT_CALL(fs, access("/",_)).Times(AnyNumber()).WillRepeatedly(Return(0)); + EXPECT_CALL(fs, getattr(_,_)).WillRepeatedly(Invoke([&removed](std::string const & path, struct stat * attr){ + memset(reinterpret_cast(attr),0, sizeof(struct stat)); + + if (path == "/") + { + attr->st_nlink = 1; + attr->st_mode = S_IFDIR | 0755; + return 0; + } + if ((path == "/some_dir") && (!removed)) + { + attr->st_nlink = 1; + attr->st_mode = S_IFDIR | 0644; + return 0; + } + else + { + return -ENOENT; + } + })); + EXPECT_CALL(fs, rmdir("/some_dir")).WillOnce(Invoke([&removed](auto const &) { + removed = true; + return 0; + })); + + webfuse::fixture fixture(fs); + auto const path = fixture.get_path() + "/some_dir"; + + ASSERT_EQ(0, ::rmdir(path.c_str())); +} diff --git a/test-src/integration/test_statfs.cpp b/test-src/integration/test_statfs.cpp new file mode 100644 index 0000000..c112c09 --- /dev/null +++ b/test-src/integration/test_statfs.cpp @@ -0,0 +1,46 @@ +#include "webfuse/webfuse.hpp" +#include "webfuse/test/fixture.hpp" +#include "webfuse/test/filesystem_mock.hpp" + +#include + +#include + +#include +#include + + +using testing::_; +using testing::Return; +using testing::Invoke; +using testing::AnyNumber; +using testing::AtMost; + +TEST(statfs, success) +{ + webfuse::filesystem_mock fs; + EXPECT_CALL(fs, access("/",_)).Times(AnyNumber()).WillRepeatedly(Return(0)); + EXPECT_CALL(fs, getattr(_,_)).WillRepeatedly(Invoke([](std::string const & path, struct stat * attr){ + memset(reinterpret_cast(attr),0, sizeof(struct stat)); + + if (path == "/") + { + attr->st_nlink = 1; + attr->st_mode = S_IFDIR | 0755; + return 0; + } + else + { + return -ENOENT; + } + })); + EXPECT_CALL(fs, statfs("/", _)).Times(AnyNumber()).WillRepeatedly(Invoke([](auto const &, struct statvfs * statistics){ + return 0; + })); + + webfuse::fixture fixture(fs); + auto const path = fixture.get_path() + "/"; + + struct statvfs statistics; + ASSERT_EQ(0, ::statvfs(path.c_str(), &statistics)); +} diff --git a/test-src/integration/test_symlink.cpp b/test-src/integration/test_symlink.cpp new file mode 100644 index 0000000..3ed881e --- /dev/null +++ b/test-src/integration/test_symlink.cpp @@ -0,0 +1,74 @@ +#include "webfuse/webfuse.hpp" +#include "webfuse/test/fixture.hpp" +#include "webfuse/test/filesystem_mock.hpp" + +#include + +using testing::_; +using testing::Return; +using testing::Invoke; +using testing::AnyNumber; + +TEST(symlink, sucessfully_create_symlink) +{ + bool link_created = false; + + webfuse::filesystem_mock fs; + EXPECT_CALL(fs, access("/",_)).Times(AnyNumber()).WillRepeatedly(Return(0)); + EXPECT_CALL(fs, getattr(_,_)).WillRepeatedly(Invoke([&link_created](std::string const & path, struct stat * attr){ + memset(reinterpret_cast(attr),0, sizeof(struct stat)); + + if (path == "/") + { + attr->st_nlink = 1; + attr->st_mode = S_IFDIR | 0755; + return 0; + } + else if ((path == "/some_link") && (link_created)) + { + attr->st_nlink = 1; + attr->st_mode = S_IFLNK | 0755; + return 0; + } + else + { + return -ENOENT; + } + })); + EXPECT_CALL(fs, symlink("link-target", "/some_link")).WillOnce(Invoke([&link_created](auto const &, auto const &){ + link_created = true; + return 0; + })); + + webfuse::fixture fixture(fs); + auto const path = fixture.get_path() + "/some_link"; + + ASSERT_EQ(0, ::symlink("link-target", path.c_str())); +} + +TEST(symlink, failed_to_create_symlink) +{ + webfuse::filesystem_mock fs; + EXPECT_CALL(fs, access("/",_)).Times(AnyNumber()).WillRepeatedly(Return(0)); + EXPECT_CALL(fs, getattr(_,_)).WillRepeatedly(Invoke([](std::string const & path, struct stat * attr){ + memset(reinterpret_cast(attr),0, sizeof(struct stat)); + + if (path == "/") + { + attr->st_nlink = 1; + attr->st_mode = S_IFDIR | 0755; + return 0; + } + else + { + return -ENOENT; + } + })); + EXPECT_CALL(fs, symlink("link-target", "/some_link")).WillOnce(Return(-EDQUOT)); + + webfuse::fixture fixture(fs); + auto const path = fixture.get_path() + "/some_link"; + + ASSERT_NE(0, ::symlink("link-target", path.c_str())); + ASSERT_EQ(EDQUOT, errno); +} diff --git a/test-src/integration/test_truncate.cpp b/test-src/integration/test_truncate.cpp new file mode 100644 index 0000000..759f963 --- /dev/null +++ b/test-src/integration/test_truncate.cpp @@ -0,0 +1,44 @@ +#include "webfuse/webfuse.hpp" +#include "webfuse/test/fixture.hpp" +#include "webfuse/test/filesystem_mock.hpp" + +#include + +using testing::_; +using testing::Return; +using testing::Invoke; +using testing::AnyNumber; + +TEST(truncate, success) +{ + bool link_created = false; + + webfuse::filesystem_mock fs; + EXPECT_CALL(fs, access("/",_)).Times(AnyNumber()).WillRepeatedly(Return(0)); + EXPECT_CALL(fs, getattr(_,_)).WillRepeatedly(Invoke([&link_created](std::string const & path, struct stat * attr){ + memset(reinterpret_cast(attr),0, sizeof(struct stat)); + + if (path == "/") + { + attr->st_nlink = 1; + attr->st_mode = S_IFDIR | 0755; + return 0; + } + if (path == "/some_file") + { + attr->st_nlink = 1; + attr->st_mode = S_IFREG | 0755; + return 0; + } + else + { + return -ENOENT; + } + })); + EXPECT_CALL(fs, truncate("/some_file", 42, _)).WillOnce(Return(0)); + + webfuse::fixture fixture(fs); + auto const path = fixture.get_path() + "/some_file"; + + ASSERT_EQ(0, ::truncate(path.c_str(), 42)); +} diff --git a/test-src/integration/test_unlink.cpp b/test-src/integration/test_unlink.cpp new file mode 100644 index 0000000..8b4f488 --- /dev/null +++ b/test-src/integration/test_unlink.cpp @@ -0,0 +1,44 @@ +#include "webfuse/webfuse.hpp" +#include "webfuse/test/fixture.hpp" +#include "webfuse/test/filesystem_mock.hpp" + +#include + +using testing::_; +using testing::Return; +using testing::Invoke; +using testing::AnyNumber; + +TEST(unlink, success) +{ + bool link_created = false; + + webfuse::filesystem_mock fs; + EXPECT_CALL(fs, access("/",_)).Times(AnyNumber()).WillRepeatedly(Return(0)); + EXPECT_CALL(fs, getattr(_,_)).WillRepeatedly(Invoke([&link_created](std::string const & path, struct stat * attr){ + memset(reinterpret_cast(attr),0, sizeof(struct stat)); + + if (path == "/") + { + attr->st_nlink = 1; + attr->st_mode = S_IFDIR | 0755; + return 0; + } + if (path == "/some_file") + { + attr->st_nlink = 1; + attr->st_mode = S_IFREG | 0755; + return 0; + } + else + { + return -ENOENT; + } + })); + EXPECT_CALL(fs, unlink("/some_file")).WillOnce(Return(0)); + + webfuse::fixture fixture(fs); + auto const path = fixture.get_path() + "/some_file"; + + ASSERT_EQ(0, ::unlink(path.c_str())); +} diff --git a/test-src/integration/test_utimens.cpp b/test-src/integration/test_utimens.cpp new file mode 100644 index 0000000..24d04c3 --- /dev/null +++ b/test-src/integration/test_utimens.cpp @@ -0,0 +1,46 @@ +#include "webfuse/webfuse.hpp" +#include "webfuse/test/fixture.hpp" +#include "webfuse/test/filesystem_mock.hpp" + +#include + +#include +#include + +using testing::_; +using testing::Return; +using testing::Invoke; +using testing::AnyNumber; + +TEST(utimens, success) +{ + webfuse::filesystem_mock fs; + EXPECT_CALL(fs, access("/",_)).Times(AnyNumber()).WillRepeatedly(Return(0)); + EXPECT_CALL(fs, getattr(_,_)).WillRepeatedly(Invoke([](std::string const & path, struct stat * attr){ + memset(reinterpret_cast(attr),0, sizeof(struct stat)); + if (path == "/") + { + attr->st_nlink = 1; + attr->st_mode = S_IFDIR | 0755; + return 0; + } + if (path == "/some_file") + { + attr->st_nlink = 1; + attr->st_mode = S_IFREG | 0644; + return 0; + } + else + { + std::cout << "getattr: failed" << std::endl; + return -ENOENT; + } + })); + EXPECT_CALL(fs, utimens("/some_file", _, _)).WillOnce(Return(0)); + + webfuse::fixture fixture(fs); + auto const path = fixture.get_path() + "/some_file"; + + timeval times[2] = {42,42}; + ASSERT_EQ(0, ::utimes(path.c_str(), times)); +} diff --git a/test-src/integration/test_write.cpp b/test-src/integration/test_write.cpp new file mode 100644 index 0000000..c53df32 --- /dev/null +++ b/test-src/integration/test_write.cpp @@ -0,0 +1,53 @@ +#include "webfuse/webfuse.hpp" +#include "webfuse/test/fixture.hpp" +#include "webfuse/test/filesystem_mock.hpp" + +#include +#include +#include + + +using testing::_; +using testing::Return; +using testing::Invoke; +using testing::AnyNumber; +using testing::AtMost; + +TEST(write, success) +{ + webfuse::filesystem_mock fs; + EXPECT_CALL(fs, access("/",_)).Times(AnyNumber()).WillRepeatedly(Return(0)); + EXPECT_CALL(fs, getattr(_,_)).WillRepeatedly(Invoke([](std::string const & path, struct stat * attr){ + memset(reinterpret_cast(attr),0, sizeof(struct stat)); + + if (path == "/") + { + attr->st_nlink = 1; + attr->st_mode = S_IFDIR | 0755; + return 0; + } + if (path == "/some_file") + { + attr->st_nlink = 1; + attr->st_mode = S_IFREG | 0644; + return 0; + } + else + { + return -ENOENT; + } + })); + EXPECT_CALL(fs, open("/some_file", _, _)).WillOnce(Return(0)); + EXPECT_CALL(fs, write("/some_file", _, _, _, _)).Times(AnyNumber()).WillRepeatedly(Invoke([](auto const &, char const * buffer, size_t buffer_size, uint64_t offset, auto){ + return buffer_size; + })); + EXPECT_CALL(fs, release("/some_file", _)).Times(AtMost(1)).WillOnce(Return(0)); + + webfuse::fixture fixture(fs); + auto const path = fixture.get_path() + "/some_file"; + + { + std::ofstream out(path); + out << "Test"; + } +} diff --git a/test-src/integration/webfuse/test/daemon.cpp b/test-src/integration/webfuse/test/daemon.cpp new file mode 100644 index 0000000..b4e0c09 --- /dev/null +++ b/test-src/integration/webfuse/test/daemon.cpp @@ -0,0 +1,37 @@ +#include "webfuse/test/daemon.hpp" + +#include +#include +#include + +#include + +namespace +{ + +std::string get_executable_path() +{ + char directory[PATH_MAX] = { '\0' }; + readlink("/proc/self/exe", directory, PATH_MAX); + dirname(directory); + + return std::string(directory) + "/webfuse"; +} + +} + +namespace webfuse +{ + +daemon::daemon(std::string const & mountpoint) +: p({get_executable_path(), "-f", mountpoint}) +{ + +} + +daemon::~daemon() +{ + p.kill(SIGINT); +} + +} diff --git a/test-src/integration/webfuse/test/daemon.hpp b/test-src/integration/webfuse/test/daemon.hpp new file mode 100644 index 0000000..b5827ae --- /dev/null +++ b/test-src/integration/webfuse/test/daemon.hpp @@ -0,0 +1,25 @@ +#ifndef WEBFUSE_DAEMOM_HPP +#define WEBFUSE_DAEMOM_HPP + +#include "webfuse/test/process.hpp" +#include + +namespace webfuse +{ + +class daemon +{ + daemon(daemon const &) = delete; + daemon& operator=(daemon const &) = delete; + daemon(daemon &&) = delete; + daemon& operator=(daemon &&) = delete; +public: + explicit daemon(std::string const & mountpoint); + ~daemon(); +private: + process p; +}; + +} + +#endif diff --git a/test-src/integration/webfuse/test/filesystem_mock.hpp b/test-src/integration/webfuse/test/filesystem_mock.hpp new file mode 100644 index 0000000..6e583ab --- /dev/null +++ b/test-src/integration/webfuse/test/filesystem_mock.hpp @@ -0,0 +1,50 @@ +#ifndef WEBFUSE_FILESYSTEM_MOCK_HPP +#define WEBFUSE_FILESYSTEM_MOCK_HPP + +#include "webfuse/filesystem/filesystem_i.hpp" +#include + +namespace webfuse +{ + +class filesystem_mock: public filesystem_i +{ +public: + ~filesystem_mock() override = default; + + MOCK_METHOD(int, access, (std::string const & path, int mode)); + MOCK_METHOD(int, getattr, (std::string const & path, struct stat * attr)); + + MOCK_METHOD(int, readlink, (std::string const & path, std::string & out)); + MOCK_METHOD(int, symlink, (std::string const & target, std::string const & linkpath)); + MOCK_METHOD(int, link, (std::string const & old_path, std::string const & new_path)); + + MOCK_METHOD(int, rename, (std::string const & old_path, std::string const & new_path, int flags)); + MOCK_METHOD(int, chmod, (std::string const & path, mode_t mode)); + MOCK_METHOD(int, chown, (std::string const & path, uid_t uid, gid_t gid)); + MOCK_METHOD(int, truncate, (std::string const & path, uint64_t size, uint64_t handle)); + MOCK_METHOD(int, fsync, (std::string const & path, bool is_datasync, uint64_t handle)); + MOCK_METHOD(int, utimens, (std::string const &path, struct timespec const tv[2], uint64_t handle)); + + MOCK_METHOD(int, open, (std::string const & path, int flags, uint64_t & handle)); + MOCK_METHOD(int, mknod, (std::string const & path, mode_t mode, dev_t rdev)); + MOCK_METHOD(int, create, (std::string const & path, mode_t mode, uint64_t & handle)); + MOCK_METHOD(int, release, (std::string const & path, uint64_t handle)); + MOCK_METHOD(int, unlink, (std::string const & path)); + + MOCK_METHOD(int, read, (std::string const & path, char * buffer, size_t buffer_size, uint64_t offset, uint64_t handle)); + MOCK_METHOD(int, write, (std::string const & path, char const * buffer, size_t buffer_size, uint64_t offset, uint64_t handle)); + + MOCK_METHOD(int, mkdir, (std::string const & path, mode_t mode)); + MOCK_METHOD(int, readdir, (std::string const & path, std::vector & entries)); + MOCK_METHOD(int, rmdir, (std::string const & path)); + + MOCK_METHOD(int, statfs, (std::string const & path, struct statvfs * statistics)); + + MOCK_METHOD(std::string, get_credentials, ()); + +}; + +} + +#endif diff --git a/test-src/integration/webfuse/test/fixture.cpp b/test-src/integration/webfuse/test/fixture.cpp new file mode 100644 index 0000000..7b377a6 --- /dev/null +++ b/test-src/integration/webfuse/test/fixture.cpp @@ -0,0 +1,63 @@ +#include "webfuse/test/fixture.hpp" +#include +#include +#include +#include + +namespace webfuse +{ + +fixture::fixture(filesystem_i & fs) +: shutdown_requested(false) +, provider_running(false) +, fs_provider(fs, "") +, app(working_dir.name()) +{ + fs_provider.set_connection_listener([this](bool is_connected) { + if (is_connected) + { + this->provider_running = true; + } + + if ((!is_connected) && (!this->provider_running)) + { + this->reconnect(); + } + }); + provider_thread = std::thread(std::bind(&fixture::provider_run, this)); + while (!provider_running) + { + std::this_thread::yield(); + } + std::this_thread::sleep_for(std::chrono::milliseconds(500)); +} + +fixture::~fixture() +{ + shutdown_requested = true; + fs_provider.interrupt(); + provider_thread.join(); +} + +void fixture::provider_run() +{ + fs_provider.connect("ws://localhost:8081/"); + while (!shutdown_requested) + { + fs_provider.service(); + } +} + +void fixture::reconnect() +{ + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + fs_provider.connect("ws://localhost:8081/"); +} + +std::string const & fixture::get_path() const +{ + return working_dir.name(); +} + + +} diff --git a/test-src/integration/webfuse/test/fixture.hpp b/test-src/integration/webfuse/test/fixture.hpp new file mode 100644 index 0000000..7d3e3e5 --- /dev/null +++ b/test-src/integration/webfuse/test/fixture.hpp @@ -0,0 +1,46 @@ +#ifndef WEBFUSE_FIXTURE_HPP +#define WEBFUSE_FIXTURE_HPP + +#include "webfuse/filesystem/filesystem_i.hpp" +#include "webfuse/webfuse.hpp" +#include "webfuse/provider.hpp" + +#include "webfuse/test/tempdir.hpp" +#include "webfuse/test/daemon.hpp" + +#include +#include +#include +#include + +namespace webfuse +{ + +class fixture +{ + fixture(fixture const &) = delete; + fixture& operator=(fixture const &) = delete; + fixture(fixture const &&) = delete; + fixture& operator=(fixture &&) = delete; +public: + explicit fixture(filesystem_i & fs); + ~fixture(); + + std::string const & get_path() const; + + void reconnect(); +private: + void provider_run(); + std::atomic shutdown_requested; + std::atomic provider_running; + provider fs_provider; + + tempdir working_dir; + daemon app; + std::thread provider_thread; +}; + + +} + +#endif diff --git a/test-src/integration/webfuse/test/process.cpp b/test-src/integration/webfuse/test/process.cpp new file mode 100644 index 0000000..2866565 --- /dev/null +++ b/test-src/integration/webfuse/test/process.cpp @@ -0,0 +1,93 @@ +#include "webfuse/test/process.hpp" + +#include +#include + +#include +#include +#include + +#include + +namespace webfuse +{ + +process::process(std::vector const & commandline) +{ + if (commandline.empty()) + { + throw std::runtime_error("missing command"); + } + + pid = fork(); + if (pid == 0) + { + size_t const count = commandline.size() + 1; + char ** args = reinterpret_cast(malloc(sizeof(char*) * count)); + args[count - 1] = nullptr; + for(size_t i = 0; i < commandline.size(); i++) + { + args[i] = strdup(commandline[i].c_str()); + } + + closefrom(0); + open("/dev/null", O_RDONLY); + open("/dev/null", O_WRONLY); + dup2(STDOUT_FILENO, STDERR_FILENO); + + execv(args[0], args); + + // this should not be reached + for(size_t i = 0; i < count; i++) + { + free(args[i]); + } + free(args); + + exit(EXIT_FAILURE); + } + else if (pid > 0) + { + // parent: do nothing + } + else + { + throw std::runtime_error("failed to fork"); + } +} + +process::~process() +{ + if (pid > 0) + { + wait(); + } +} + +void process::kill(int signal_number) +{ + if (pid > 0) + { + ::kill(pid, signal_number); + } +} + +int process::wait() +{ + int exit_code = -1; + + if (pid > 0) + { + int status = 0; + int rc = waitpid(pid, &status, 0); + if (rc == pid) + { + exit_code = WEXITSTATUS(status); + pid = 0; + } + } + + return exit_code; +} + +} diff --git a/test-src/integration/webfuse/test/process.hpp b/test-src/integration/webfuse/test/process.hpp new file mode 100644 index 0000000..3154524 --- /dev/null +++ b/test-src/integration/webfuse/test/process.hpp @@ -0,0 +1,29 @@ +#ifndef WEBFUSE_PROCESS_HPP +#define WEBFUSE_PROCESS_HPP + +#include + +#include +#include + +namespace webfuse +{ + +class process +{ + process(process const &) = delete; + process operator=(process const &) = delete; + process(process &&) = delete; + process operator=(process &&) = delete; +public: + process(std::vector const & commandline); + ~process(); + void kill(int signal_number); + int wait(); +private: + pid_t pid; +}; + +} + +#endif diff --git a/test-src/integration/webfuse/test/tempdir.cpp b/test-src/integration/webfuse/test/tempdir.cpp new file mode 100644 index 0000000..5be4f0f --- /dev/null +++ b/test-src/integration/webfuse/test/tempdir.cpp @@ -0,0 +1,23 @@ +#include "webfuse/test/tempdir.hpp" +#include + +namespace webfuse +{ + +tempdir::tempdir() +{ + char path_template[] = "/tmp/webfuse_test_XXXXXX"; + path = mkdtemp(path_template); +} + +tempdir::~tempdir() +{ + rmdir(path.c_str()); +} + +std::string const & tempdir::name() const +{ + return path; +} + +} diff --git a/test-src/integration/webfuse/test/tempdir.hpp b/test-src/integration/webfuse/test/tempdir.hpp new file mode 100644 index 0000000..0e73894 --- /dev/null +++ b/test-src/integration/webfuse/test/tempdir.hpp @@ -0,0 +1,22 @@ +#ifndef WEBFUSE_TEMPDIR_HPP +#define WEBFUSE_TEMPDIR_HPP + +#include + +namespace webfuse +{ + +class tempdir +{ +public: + tempdir(); + ~tempdir(); + std::string const & name() const; +private: + std::string path; + +}; + +} + +#endif diff --git a/test-src/unit/webfuse/filesystem/test_accessmode.cpp b/test-src/unit/webfuse/filesystem/test_accessmode.cpp new file mode 100644 index 0000000..1f265cc --- /dev/null +++ b/test-src/unit/webfuse/filesystem/test_accessmode.cpp @@ -0,0 +1,26 @@ +#include "webfuse/filesystem/accessmode.hpp" +#include + +using webfuse::access_mode; + +TEST(accessmode, f_ok) +{ + ASSERT_EQ(0, access_mode::f_ok); + ASSERT_EQ(F_OK, access_mode::f_ok); +} + +class accessmode_test: public testing::TestWithParam { }; + +TEST_P(accessmode_test, conversion) +{ + int const expected = GetParam(); + auto mode = access_mode::from_int(expected); + ASSERT_EQ(expected, mode.to_int()); +} + +INSTANTIATE_TEST_CASE_P(accesmode_values, accessmode_test, + testing::Values( + F_OK, R_OK, W_OK, X_OK, + R_OK | W_OK, R_OK | X_OK, W_OK | X_OK, + R_OK | W_OK | X_OK) +); diff --git a/test-src/unit/webfuse/filesystem/test_filemode.cpp b/test-src/unit/webfuse/filesystem/test_filemode.cpp new file mode 100644 index 0000000..9330d4c --- /dev/null +++ b/test-src/unit/webfuse/filesystem/test_filemode.cpp @@ -0,0 +1,25 @@ +#include "webfuse/filesystem/filemode.hpp" +#include + +using webfuse::filemode; + +class filemode_test: public testing::TestWithParam { }; + +TEST_P(filemode_test, conversion) +{ + mode_t const expected = GetParam(); + auto value = filemode::from_mode(expected); + ASSERT_EQ(expected, value.to_mode()); +} + +INSTANTIATE_TEST_CASE_P(filemode_value, filemode_test, + testing::Values( + S_IROTH, S_IWOTH, S_IXOTH, + S_IRGRP, S_IWGRP, S_IXGRP, + S_IRUSR, S_IWUSR, S_IXUSR, + S_ISUID, S_ISGID, S_ISVTX, + S_IFREG, S_IFCHR, S_IFBLK, S_IFDIR, S_IFIFO, S_IFLNK, S_IFSOCK, + S_IFREG | 0644, + S_IFDIR | 0755 + ) +); diff --git a/test-src/unit/webfuse/filesystem/test_openflags.cpp b/test-src/unit/webfuse/filesystem/test_openflags.cpp new file mode 100644 index 0000000..db9b74c --- /dev/null +++ b/test-src/unit/webfuse/filesystem/test_openflags.cpp @@ -0,0 +1,24 @@ +#include "webfuse/filesystem/openflags.hpp" +#include +#include + +using webfuse::openflags; + +class openflags_test: public testing::TestWithParam { }; + +TEST_P(openflags_test, conversion) +{ + int const expected = GetParam(); + auto flags = openflags::from_int(expected); + ASSERT_EQ(expected, flags.to_int()); +} + +INSTANTIATE_TEST_CASE_P(openflags_values, openflags_test, + testing::Values<>( + O_RDONLY, O_WRONLY, O_RDWR, O_CLOEXEC, O_CREAT, + O_DIRECT, O_DIRECTORY, O_EXCL, O_NOCTTY, O_NOFOLLOW, + O_TRUNC, O_ASYNC, O_LARGEFILE, O_NOATIME, O_NONBLOCK, + O_NDELAY, O_SYNC, + O_WRONLY | O_CREAT | O_TRUNC + ) +); diff --git a/test-src/unit/webfuse/filesystem/test_status.cpp b/test-src/unit/webfuse/filesystem/test_status.cpp new file mode 100644 index 0000000..52dcc7b --- /dev/null +++ b/test-src/unit/webfuse/filesystem/test_status.cpp @@ -0,0 +1,28 @@ +#include "webfuse/filesystem/status.hpp" +#include +#include + +using webfuse::status; + +class status_test: public testing::TestWithParam { }; + +TEST_P(status_test, conversion) +{ + int const expected = GetParam(); + auto status = status::from_fusestatus(expected); + ASSERT_EQ(expected, status.to_fusestatus()); +} + +INSTANTIATE_TEST_CASE_P(status_values, status_test, + testing::Values( + 0, 1, 2, 3, 42, + -E2BIG, -EACCES, -EBADF, -EBUSY, -EDESTADDRREQ, + -EDQUOT, -EEXIST, -EFAULT, -EFBIG, -EINTR, + -EINVAL, -EIO, -EISDIR, -ELOOP, -EMFILE, + -ENAMETOOLONG, -ENFILE, -ENODATA, -ENODEV, + -ENOENT, -ENOMEM, -ENOSPC, -ENOSYS, -ENOTDIR, + -ENOTEMPTY, -ENOTSUP, -ENXIO, -EOVERFLOW, -EPERM, + -EPIPE, -ERANGE, -EROFS, -EXDEV, -EWOULDBLOCK, + -EAGAIN, -12345 + ) +); diff --git a/test-src/unit/webfuse/test_app.cpp b/test-src/unit/webfuse/test_app.cpp new file mode 100644 index 0000000..77d4f56 --- /dev/null +++ b/test-src/unit/webfuse/test_app.cpp @@ -0,0 +1,7 @@ +#include "webfuse/webfuse.hpp" +#include + +TEST(app, init) +{ + webfuse::app app; +} diff --git a/test-src/unit/webfuse/test_request_type.cpp b/test-src/unit/webfuse/test_request_type.cpp new file mode 100644 index 0000000..4f49639 --- /dev/null +++ b/test-src/unit/webfuse/test_request_type.cpp @@ -0,0 +1,37 @@ +#include "webfuse/request_type.hpp" +#include + +using webfuse::request_type; + +class request_type_test: public testing::TestWithParam { }; + +TEST_P(request_type_test, conversion) +{ + auto const expected = GetParam(); + auto const actual = webfuse::get_request_type(static_cast(expected)); + + ASSERT_EQ(expected, actual); +} + +INSTANTIATE_TEST_CASE_P(request_type_values, request_type_test, + testing::Values( + request_type::access, request_type::getattr, request_type::readlink, + request_type::symlink, request_type::link, request_type::link, + request_type::rename, request_type::chmod, request_type::chown, + request_type::truncate, request_type::fsync, request_type::open, + request_type::mknod, request_type::create, request_type::release, + request_type::unlink, request_type::read, request_type::write, + request_type::mkdir, request_type::readdir, request_type::rmdir, + request_type::statfs, request_type::utimens, request_type::unknown) +); + +TEST(request_type, unknown_values) +{ + auto const expected = request_type::unknown; + + ASSERT_EQ(expected, webfuse::get_request_type(0x20)); + ASSERT_EQ(expected, webfuse::get_request_type(0x30)); + ASSERT_EQ(expected, webfuse::get_request_type(0x80)); + ASSERT_EQ(expected, webfuse::get_request_type(0x42)); + ASSERT_EQ(expected, webfuse::get_request_type(0xff)); +} diff --git a/test-src/unit/webfuse/test_response_type.cpp b/test-src/unit/webfuse/test_response_type.cpp new file mode 100644 index 0000000..13e1744 --- /dev/null +++ b/test-src/unit/webfuse/test_response_type.cpp @@ -0,0 +1,38 @@ +#include "webfuse/response_type.hpp" +#include + +using webfuse::request_type; + +class response_type_test: public testing::TestWithParam { }; + +TEST_P(response_type_test, conversion) +{ + auto const value = GetParam(); + auto const actual = webfuse::get_response_type(value); + auto const expected = static_cast(static_cast(value) | 0x80); + + ASSERT_EQ(expected, actual); +} + +INSTANTIATE_TEST_CASE_P(response_type_values, response_type_test, + testing::Values( + request_type::access, request_type::getattr, request_type::readlink, + request_type::symlink, request_type::link, request_type::link, + request_type::rename, request_type::chmod, request_type::chown, + request_type::truncate, request_type::fsync, request_type::open, + request_type::mknod, request_type::create, request_type::release, + request_type::unlink, request_type::read, request_type::write, + request_type::mkdir, request_type::readdir, request_type::rmdir, + request_type::statfs, request_type::utimens, request_type::unknown) +); + +TEST(respones_type, unknown_values) +{ + auto const expected = webfuse::response_type::unknown; + + ASSERT_EQ(expected, webfuse::get_response_type(static_cast(0x20))); + ASSERT_EQ(expected, webfuse::get_response_type(static_cast(0x30))); + ASSERT_EQ(expected, webfuse::get_response_type(static_cast(80))); + ASSERT_EQ(expected, webfuse::get_response_type(static_cast(0x42))); + ASSERT_EQ(expected, webfuse::get_response_type(static_cast(0xff))); +}