Compare commits

...

24 Commits
0.7.4 ... main

Author SHA1 Message Date
tastytea 8a91017272
Explain supported image formats
continuous-integration/drone/push Build is passing Details
2022-06-26 16:05:26 +02:00
tastytea 849762eba6
fix nginx example URL and dependency versions in readme
continuous-integration/drone/push Build is passing Details
2022-06-25 13:31:10 +02:00
tastytea 58791fce82
Version bump 0.9.0
continuous-integration/drone/push Build is passing Details
2022-06-11 09:06:04 +02:00
tastytea ed9b0bfcfb
add note about default branch name change
continuous-integration/drone/push Build was killed Details
2022-06-11 09:04:17 +02:00
tastytea 89edcb4de7
Fix install in CI
continuous-integration/drone/push Build is passing Details
2022-06-11 08:58:37 +02:00
tastytea a1f2309dc7
typo 2022-06-11 08:43:56 +02:00
tastytea 7328f93b6e
Add git to CI
continuous-integration/drone/push Build is passing Details
2022-06-11 08:42:42 +02:00
tastytea 528f4480aa
Use FetchContent for identiconpp
continuous-integration/drone/push Build is failing Details
2022-06-11 08:33:58 +02:00
tastytea 80ccc2c7a1
CI: fix identiconpp discovery?
continuous-integration/drone/push Build is failing Details
2022-06-11 07:55:41 +02:00
tastytea d927ef6ca0
remove libxdg-basedir from docs and CI 2022-06-11 07:52:25 +02:00
tastytea dc5d843c08
drone: get submodules
continuous-integration/drone/push Build is failing Details
2022-06-11 07:48:26 +02:00
tastytea 65004ce1f8
Update drone recipe and docs
continuous-integration/drone/push Build is failing Details
2022-06-11 07:42:56 +02:00
tastytea 3039d16919
Bundle identiconpp 2022-06-11 07:35:29 +02:00
tastytea 2b9575c51a
actually pass file type to image::write() 2022-06-11 06:40:00 +02:00
tastytea e4b246bc75
remove debug statement 2022-06-11 06:27:26 +02:00
tastytea 78a7184250
allow requesting file type, try to guess right quality 2022-06-11 06:16:12 +02:00
tastytea 6aa3b40faa
Read images from file, allow forcing magick 2022-06-11 05:28:55 +02:00
tastytea 17205298cd
implement fallbacks 2022-06-11 04:46:54 +02:00
tastytea cb354e666d
move types to types.hpp, match hash to email 2022-06-11 04:29:05 +02:00
tastytea cbd86c7000
Generate hashtable 2022-06-09 20:53:26 +02:00
tastytea 03ae11a842
Parse REQUEST_URI 2022-06-09 04:28:50 +02:00
tastytea 7316fe90e3
start of rewrite
- update CMake files
- add QA and formatting configs
2022-06-09 02:31:30 +02:00
tastytea 41abb50563
Version bump 0.7.5
continuous-integration/drone/push Build is passing Details
Also bumped the maximum supported CMake version while I was in the file.
2021-11-26 04:51:31 +01:00
tastytea 3433f88dcc
Don't allow redirections to external websites.
continuous-integration/drone/push Build is passing Details
See <https://cwe.mitre.org/data/definitions/601.html>.
2021-11-26 04:45:59 +01:00
29 changed files with 1057 additions and 772 deletions

119
.clang-format Normal file
View File

@ -0,0 +1,119 @@
# -*- mode: yaml -*-
# Written for clang-format 13.
# https://releases.llvm.org/13.0.0/tools/clang/docs/ClangFormatStyleOptions.html
---
DisableFormat: false
Language: Cpp
AccessModifierOffset: -4
AlignAfterOpenBracket: Align
AlignArrayOfStructures: Left
AlignConsecutiveAssignments: None
AlignConsecutiveBitFields: None
AlignConsecutiveDeclarations: None
AlignConsecutiveMacros: None
AlignEscapedNewlines: DontAlign
AlignOperands: Align
AlignTrailingComments: true
AllowAllArgumentsOnNextLine: false
AllowAllConstructorInitializersOnNextLine: false
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortBlocksOnASingleLine: Empty
AllowShortCaseLabelsOnASingleLine: true
AllowShortEnumsOnASingleLine: false
AllowShortFunctionsOnASingleLine: Empty
AllowShortIfStatementsOnASingleLine: Never
AllowShortLambdasOnASingleLine: Inline
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: Yes
# AttributeMacros: ['__capability', '__output', '__ununsed']
BinPackArguments: true
BinPackParameters: true
BitFieldColonSpacing: Both
# BraceWrapping: # If BreakBeforeBraces is set to Custom.
BreakBeforeBinaryOperators: NonAssignment
BreakBeforeBraces: Attach
BreakBeforeConceptDeclarations: true
BreakBeforeTernaryOperators: true
BreakConstructorInitializers: BeforeComma
BreakInheritanceList: BeforeComma
BreakStringLiterals: true
ColumnLimit: 80
# CommentPragmas:
CompactNamespaces: false
ConstructorInitializerAllOnOneLineOrOnePerLine: false
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
Cpp11BracedListStyle: true
DeriveLineEnding: true
DerivePointerAlignment: false
EmptyLineBeforeAccessModifier: LogicalBlock
FixNamespaceComments: true
# ForEachMacros: ['FOREACH', 'RANGES_FOR', 'Q_FOREACH', 'BOOST_FOREACH' ]
# IfMacros: ['IF']
IncludeBlocks: Regroup
IncludeCategories: # stdlib headers into own group.
- Regex: '^[^\.]+$'
Priority: 4
IndentAccessModifiers: false
IndentCaseBlocks: false
IndentCaseLabels: false
IndentExternBlock: NoIndent
IndentGotoLabels: false
IndentPPDirectives: AfterHash
IndentRequires: true # Not sure yet
IndentWidth: 4
IndentWrappedFunctionNames: false
InsertTrailingCommas: None
KeepEmptyLinesAtTheStartOfBlocks: true
LambdaBodyIndentation: Signature
# MacroBlockBegin:
# MacroBlockEnd:
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
# NamespaceMacros:
PenaltyBreakAssignment: 250
PenaltyBreakBeforeFirstCallParameter: 300
# PenaltyBreakComment: 300
# PenaltyBreakFirstLessLess: 120
# PenaltyBreakString: 1000
# PenaltyBreakTemplateDeclaration: 10
# PenaltyExcessCharacter: 1000000
# PenaltyIndentedWhitespace:
PenaltyReturnTypeOnItsOwnLine: 100
PointerAlignment: Right
ReflowComments: true
SortIncludes: CaseInsensitive
SortUsingDeclarations: true
SpaceAfterCStyleCast: false
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: false
SpaceAroundPointerQualifiers: Default
SpaceBeforeAssignmentOperators: true
SpaceBeforeCaseColon: false
SpaceBeforeCpp11BracedList: false
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatements
SpaceBeforeRangeBasedForLoopColon: true
SpaceBeforeSquareBrackets: false
SpaceInEmptyBlock: false
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: false
SpacesInCStyleCastParentheses: false
SpacesInConditionalStatement: false
SpacesInContainerLiterals: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
Standard: Auto
StatementAttributeLikeMacros: [emit]
# StatementMacros: [Q_UNUSED, QT_REQUIRE_VERSION]
TabWidth: 4
# TypenameMacros: ['STACK_OF', 'LIST']
UseCRLF: false
UseTab: Never
# WhitespaceSensitiveMacros: ['STRINGIZE', 'PP_STRINGIZE']
...

51
.clang-tidy Normal file
View File

@ -0,0 +1,51 @@
# -*- mode: conf; fill-column: 100; -*-
# Written for clang-tidy 13.
---
Checks: '*,
-cppcoreguidelines-non-private-member-variables-in-classes,
-fuchsia-default-arguments-calls,
-fuchsia-default-arguments-declarations,
-fuchsia-default-arguments,
-llvm-include-order,
-llvm-header-guard,
-misc-non-private-member-variables-in-classes,
-fuchsia-overloaded-operator,
-cppcoreguidelines-avoid-magic-numbers,
-readability-magic-numbers,
-cppcoreguidelines-pro-bounds-array-to-pointer-decay,
-hicpp-no-array-decay,
-modernize-avoid-c-arrays,
-cppcoreguidelines-avoid-c-arrays,
-hicpp-avoid-c-arrays,
-google-build-using-namespace,
-readability-named-parameter,
-google-runtime-references,
-hicpp-avoid-goto,
-hicpp-vararg,
-fuchsia-statically-constructed-objects,
-google-readability-todo,
-modernize-use-trailing-return-type,
-fuchsia-multiple-inheritance,
-llvmlibc*,
-cppcoreguidelines-avoid-non-const-global-variables,
-cert-*-c,
-abseil-string-find-*,
-altera-unroll-loops,
-altera-id-dependent-backward-branch,
-altera-struct-pack-align'
FormatStyle: file # Use .clang-format.
CheckOptions: # ↓ Clashes with static private member prefix. (static int _var;) ↓
- { key: readability-identifier-naming.VariableCase, value: lower_case }
- { key: readability-identifier-naming.MemberCase, value: lower_case }
- { key: readability-identifier-naming.PrivateMemberCase, value: lower_case }
- { key: readability-identifier-naming.PrivateMemberPrefix, value: _ }
- { key: readability-identifier-naming.ProtectedMemberCase, value: lower_case }
- { key: readability-identifier-naming.ProtectedMemberPrefix, value: _ }
- { key: readability-identifier-naming.ClassCase, value: lower_case }
- { key: readability-identifier-naming.StructCase, value: lower_case }
- { key: readability-identifier-naming.EnumCase, value: lower_case }
- { key: readability-identifier-naming.FunctionCase, value: lower_case }
- { key: readability-identifier-naming.ParameterCase, value: lower_case }
...

View File

@ -13,43 +13,6 @@ trigger:
- tag
steps:
# - name: Download identiconpp for Debian stretch
# image: plugins/download
# settings:
# source: https://schlomp.space/attachments/f40d4b6e-ab9f-4cb7-a83e-cb1d8490e9ee
# destination: identiconpp_stretch.deb
- name: Download identiconpp for Debian buster
image: plugins/download
settings:
source: https://schlomp.space/attachments/a94b0252-db1e-4122-bc27-cb6679acb3af
destination: identiconpp_buster.deb
# - name: GCC 6
# image: debian:stretch-slim
# pull: always
# environment:
# CXX: g++-6
# CXXFLAGS: -pipe -O2
# DEBIAN_FRONTEND: noninteractive
# LANG: C.utf8
# commands:
# - rm /etc/apt/apt.conf.d/docker-clean
# - echo "APT::Default-Release \"stretch\";" >> /etc/apt/apt.conf.d/00default_release
# - echo "deb http://deb.debian.org/debian stretch-backports main" >> /etc/apt/sources.list.d/stretch.list
# - apt-get update -q
# - apt-get install -qq -t stretch-backports cmake
# - apt-get install -qq g++-6 pkg-config
# - apt-get install -qq libcrypto++-dev libmagick++-dev libxdg-basedir-dev
# - dpkg -i identiconpp_stretch.deb
# - rm -rf build && mkdir -p build && cd build
# - cmake ..
# - cmake --build .
# - make install
# volumes:
# - name: debian-package-cache
# path: /var/cache/apt/archives
- name: GCC 8 / clang 7
image: debian:buster-slim
pull: always
@ -61,18 +24,14 @@ steps:
commands:
- rm /etc/apt/apt.conf.d/docker-clean
- apt-get update -q
- apt-get install -qq build-essential clang pkg-config cmake
- apt-get install -qq libcrypto++-dev libmagick++-dev libxdg-basedir-dev
- dpkg -i identiconpp_buster.deb
- rm -rf build && mkdir -p build && cd build
- cmake ..
- cmake --build .
- make install
- cd ../
- rm -rf build && mkdir -p build && cd build
- CXX="clang++" cmake ..
- cmake --build .
- make install
- apt-get install -qq build-essential clang pkg-config cmake git libcrypto++-dev libmagick++-dev
- rm -rf build
- cmake -S . -B build -DLIBRAVATAR_BUNDLED_IDENTICONPP=YES
- cmake --build build
- rm -rf build
- CXX="clang++" cmake -S . -B build -DLIBRAVATAR_BUNDLED_IDENTICONPP=YES
- cmake --build build
- cd build && make install
volumes:
- name: debian-package-cache
path: /var/cache/apt/archives
@ -81,7 +40,7 @@ steps:
image: drillster/drone-email
pull: always
settings:
host: cryptoparty-celle.de
host: tzend.de
from: drone@tzend.de
username:
from_secret: email_username

20
.editorconfig Normal file
View File

@ -0,0 +1,20 @@
# Configuration file for EditorConfig.
# More information is available under <https://editorconfig.org/>.
root = true
[*]
indent_style = space
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 80
[*.?pp]
indent_size = 4
tab_width = 4
[{CMakeLists.txt,*.cmake}]
indent_size = 2
tab_width = 2

0
.gitmodules vendored Normal file
View File

View File

@ -1,16 +1,23 @@
cmake_minimum_required(VERSION 3.10...3.17)
cmake_minimum_required(VERSION 3.12...3.20)
# Global build options.
set(CMAKE_BUILD_TYPE "Release" CACHE STRING "The type of build.")
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# project build option
option(LIBRAVATAR_BUNDLED_IDENTICONPP "use bundled identiconpp" NO)
project(libravatarserv
VERSION 0.7.4
VERSION 0.9.0
DESCRIPTION "A simple libravatar server."
HOMEPAGE_URL "https://schlomp.space/tastytea/libravatarserv"
LANGUAGES CXX)
list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake")
include(GNUInstallDirs)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
include(cmake/debug_flags.cmake)
find_package(PkgConfig REQUIRED)
pkg_check_modules(Magick++ REQUIRED IMPORTED_TARGET Magick++)
@ -19,30 +26,29 @@ if(NOT libcryptopp_FOUND)
# Debian stretch package installs libcrypto++.pc
pkg_check_modules(libcryptopp REQUIRED IMPORTED_TARGET libcrypto++)
endif()
pkg_check_modules(libxdg-basedir REQUIRED IMPORTED_TARGET libxdg-basedir)
find_package(identiconpp REQUIRED CONFIG)
if (LIBRAVATAR_BUNDLED_IDENTICONPP)
include(FetchContent)
FetchContent_Declare(identiconpp
GIT_REPOSITORY https://schlomp.space/tastytea/identiconpp.git
GIT_TAG 0.7.3
)
if (NOT CMAKE_VERSION VERSION_LESS 3.14)
FetchContent_MakeAvailable(identiconpp)
else()
FetchContent_GetProperties(identiconpp)
if(NOT identiconpp_POPULATED)
FetchContent_Populate(identiconpp)
add_subdirectory(${identiconpp_SOURCE_DIR} ${identiconpp_BINARY_DIR})
endif()
endif()
add_library(identiconpp::identiconpp ALIAS identiconpp)
else()
find_package(identiconpp REQUIRED CONFIG)
endif()
find_package(Filesystem REQUIRED COMPONENTS Final Experimental)
include(cmake/debug_flags.cmake)
include_directories(${PROJECT_BINARY_DIR})
# Write version in header
configure_file(
"src/version.hpp.in"
"version.hpp")
# Include <filesystem> or <experimental/filesystem>.
configure_file("src/fs-compat.hpp.in" "fs-compat.hpp" @ONLY)
file(GLOB sources src/*.cpp)
add_executable(${PROJECT_NAME} ${sources})
target_include_directories(${PROJECT_NAME}
PRIVATE ${PROJECT_BINARY_DIR})
target_link_libraries(${PROJECT_NAME}
PRIVATE PkgConfig::Magick++ PkgConfig::libcryptopp PkgConfig::libxdg-basedir
std::filesystem identiconpp::identiconpp)
install(TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR})
install(FILES README.md DESTINATION ${CMAKE_INSTALL_DOCDIR})
add_subdirectory(src)
include(cmake/packages.cmake)

37
CMakePresets.json Normal file
View File

@ -0,0 +1,37 @@
{
"version": 2,
"cmakeMinimumRequired": {
"major": 3,
"minor": 20,
"patch": 0
},
"configurePresets": [
{
"name": "common",
"hidden": true,
"generator": "Unix Makefiles",
"binaryDir": "build",
"cacheVariables": {
"CMAKE_EXPORT_COMPILE_COMMANDS": true
}
},
{
"name": "dev",
"displayName": "Developer config",
"description": "Build with debug symbols",
"inherits": "common",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "release",
"displayName": "Release config",
"description": "Build without debug symbols or tests",
"inherits": "common",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
}
]
}

View File

@ -5,6 +5,8 @@ It is intended to be used as a
[Libravatar](https://www.libravatar.org/) is a free service and an open
specification for hosting profile images tied to email or OpenID addresses.
**The default branch changed from `master` to `main` on 2022-06-11.**
## Features
* Avatar delivery based on email addresses
@ -12,7 +14,9 @@ specification for hosting profile images tied to email or OpenID addresses.
* MD5 hashes
* SHA256 hashes
* Variable image size (`s` or `size`)
* Default fallbacks (`d` or `default`): 404, URL, mp/mm, identicon, retro
* Default fallbacks (`d` or `default`): 404, mp/mm, identicon (=retro), retro
* Requesting file type by appending `.jpg`, `.png`, `.gif` or `.webp` to the
hash
The API is explained in greater detail at the
[Libravar wiki](https://wiki.libravatar.org/api/).
@ -24,12 +28,15 @@ The API is explained in greater detail at the
* The default fallbacks monsterid, wavatar, robohash and pagan
* Patches welcome
* forcedefault
* Fallback URLs for to external sites (due to [CWE-601](https://cwe.mitre.org/data/definitions/601.html))
* We have a server setting (`LIBRAVATARSERV_REDIRECT`) to redirect to
libravatar.org.
## Usage
Install nginx and
[fcgiwrap](https://www.nginx.com/resources/wiki/start/topics/examples/fcgiwrap/),
copy the [example config](https://schlomp.space/tastytea/libravatarserv/src/branch/master/doc/nginx-example.conf)
copy the [example config](https://schlomp.space/tastytea/libravatarserv/src/branch/main/doc/nginx-example.conf)
to the nginx configuration directory and edit it according to your needs. Other
webservers and cgi spawners will also work, of course.
@ -42,9 +49,10 @@ _avatars-sec._tcp.example.com. IN SRV 0 0 443 avatars.example.com
`_avatars._tcp.example.com` is for HTTP, `_avatars-sec._tcp.example.com` is for
HTTPS.
libravatarserv looks in each of `${XDG_DATA_DIRS}` for a directory named
`libravatarserv`. You can force a different directory by setting the environment
variable `LIBRAVATARSERV_DIR` (until 0.3.0 `AVATAR_DIR`).
libravatarserv looks in the current working directory for images. You can force
a different directory by setting the environment variable
`LIBRAVATARSERV_DIR`. Any file format supported by ImageMagick is supported, but
it should be PNG, JPEG or GIF for compatibility.
The image files are named like your email address, no file extension. The
default behaviour for unknown users is to return a 404 error. You can change
@ -65,7 +73,7 @@ Test your setup on <https://www.libravatar.org/tools/check/>.
The avatar directory could look like this:
``` plain
/usr/share/libravatarserv
/var/db/libravatarserv
├── [ 32K] default
├── [ 759] user@example.com
└── [ 16] user+newsletter@example.com -> user@example.com
@ -79,7 +87,7 @@ Configuration is done through environment variables.
The directory containing the avatars.
Default: empty
Default: current directory
**LIBRAVATARSERV_DEFAULT_FALLBACK**
@ -89,25 +97,18 @@ parameter.
Default: 404
**LIBRAVATARSERV_REDIRECT_NOFALLBACK**
Set to 1 to redirect to libravatar.org if the user is not found and the
requested fallback is not supported.
Default: 0
**LIBRAVATARSERV_REDIRECT_NOUSER**
**LIBRAVATARSERV_REDIRECT**
Set to 1 to redirect to libravatar.org if the user is not found.
Default: 0
Default: 1
### Things to keep in mind
libravatarserv resizes images on the fly and calculates both MD5 and SHA256
hashes for every user on every request. This could seriously strain the
ressources of your computer if many users use the service. Make sure to set up
caching if you expect more than occasional traffic.
libravatarserv resizes images on the fly and potentially calculates hashes for
every user on every request. This could seriously strain the ressources of your
computer if many users use the service. Make sure to set up caching if you
expect more than occasional traffic.
## Install
@ -120,35 +121,31 @@ Gentoo ebuilds are available via my
#### Dependencies
* C++ compiler (tested: [gcc](https://gcc.gnu.org/) 8/9,
* C++ compiler (tested: [gcc](https://gcc.gnu.org/) 8/11,
[clang](https://llvm.org/) 7)
* [cmake](https://cmake.org/) (at least 3.10)
* [crypto++](https://cryptopp.com) (tested: 7.0 / 5.6)
* [imagemagick](https://www.imagemagick.org/) (tested: 7.0 / 6.7)
* [libxdg-basedir](http://repo.or.cz/w/libxdg-basedir.git) (tested: 1.2)
* [cmake](https://cmake.org/) (at least 3.12)
* [crypto++](https://cryptopp.com) (tested: 8.6 / 5.6)
* [imagemagick](https://www.imagemagick.org/) (tested: 7.1 / 6.7)
* [identiconpp](https://schlomp.space/tastytea/identiconpp) (at least: 0.7.1)
On a Debian system, install the packages: `build-essential cmake libcrypto++-dev
libmagick++-dev libxdg-basedir-dev` and
[identiconpp](https://schlomp.space/tastytea/identiconpp).
On a Debian system, install the packages: `build-essential pkg-config cmake
libcrypto++-dev libmagick++-dev` and
[identiconpp](https://schlomp.space/tastytea/identiconpp) (or use the bundled).
#### Compile
``` shell
mkdir build && cd build
cmake ..
make
make install
cmake -S . -B build
cmake --build build
```
##### cmake options
* `-DCMAKE_BUILD_TYPE=Debug` for a debug build
* One of:
* `-DWITH_DEB=YES` to generate a deb-package
* `-DWITH_RPM=YES` to generate an rpm-package
* `-DLIBRAVATAR_BUNDLED_IDENTICONPP=YES` to use the bundled identiconpp
To generate a binary package, execute `make package`
To install, run `sudo cmake --install build`. To create a linux distribution
package, run `cpack -G DEB` or `cpack -G RPM`.
## Contributing
@ -163,6 +160,8 @@ avatars. The tests were done with a 27KiB image, scaled down from 569px to
512px. It took 3,8s / 5-7s to transfer 50 unique avatars (about 3 KiB each) on
one page.
This test was done on an older version and may not be accurate any more.
## Contact
See https://tastytea.de/

View File

@ -24,9 +24,13 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang"
"-Wdouble-promotion"
"-Wformat=2"
"-ftrapv"
"-fsanitize=undefined"
"-Og"
"-fno-omit-frame-pointer")
if(WITH_SANITIZERS)
list(APPEND tmp_CXXFLAGS
"-fsanitize=undefined"
"-fsanitize=address")
endif()
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU")
list(APPEND tmp_CXXFLAGS
"-Wlogical-op"
@ -44,8 +48,11 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang"
endif()
add_compile_options("$<$<CONFIG:Debug>:${tmp_CXXFLAGS}>")
list(APPEND tmp_LDFLAGS
"-fsanitize=undefined")
if(WITH_SANITIZERS)
list(APPEND tmp_LDFLAGS
"-fsanitize=undefined"
"-fsanitize=address")
endif()
# add_link_options was introduced in version 3.13.
if(${CMAKE_VERSION} VERSION_LESS 3.13)
set(CMAKE_SHARED_LINKER_FLAGS_DEBUG "${tmp_LDFLAGS}")

View File

@ -1,44 +1,57 @@
# Packages
set(CPACK_PACKAGE_NAME ${CMAKE_PROJECT_NAME})
set(CPACK_PACKAGE_VERSION_MAJOR ${${CMAKE_PROJECT_NAME}_VERSION_MAJOR})
set(CPACK_PACKAGE_VERSION_MINOR ${${CMAKE_PROJECT_NAME}_VERSION_MINOR})
set(CPACK_PACKAGE_VERSION_PATCH ${${CMAKE_PROJECT_NAME}_VERSION_PATCH})
set(CPACK_PACKAGE_VERSION ${${CMAKE_PROJECT_NAME}_VERSION})
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Simple libravatar server.")
set(CPACK_PACKAGE_CONTACT "tastytea <tastytea@tastytea.de>")
set(CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE")
set(CPACK_RESOURCE_FILE_README "${PROJECT_SOURCE_DIR}/README.md")
list(APPEND CPACK_SOURCE_IGNORE_FILES "/\\\\.git"
"/.gitignore"
"/build/"
"\\\\.sublime-"
"/.drone.yml")
execute_process(COMMAND uname -m
OUTPUT_VARIABLE CPACK_PACKAGE_ARCHITECTURE
OUTPUT_STRIP_TRAILING_WHITESPACE)
set(CPACK_PACKAGE_FILE_NAME
"${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}_${CPACK_PACKAGE_ARCHITECTURE}")
set(CPACK_GENERATOR "TGZ")
set(CPACK_SOURCE_GENERATOR "TGZ")
include(GNUInstallDirs)
if (WITH_DEB)
set(CPACK_GENERATOR "DEB")
set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "https://schlomp.space/tastytea/${CMAKE_PROJECT_NAME}")
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
execute_process(COMMAND dpkg --print-architecture
OUTPUT_VARIABLE CPACK_DEBIAN_PACKAGE_ARCHITECTURE
OUTPUT_STRIP_TRAILING_WHITESPACE)
set(CPACK_PACKAGE_FILE_NAME
"${CPACK_PACKAGE_NAME}_${CPACK_PACKAGE_VERSION}-0_${CPACK_DEBIAN_PACKAGE_ARCHITECTURE}")
set(CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE")
set(CPACK_PACKAGE_CONTACT "tastytea <tastytea@tastytea.de>")
# Should be set automatically, but they are not.
set(CPACK_PACKAGE_NAME "${PROJECT_NAME}")
set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "${CMAKE_PROJECT_DESCRIPTION}")
# DEB
# Figure out dependencies automatically.
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
# Should be set automatically, but it is not.
execute_process(COMMAND dpkg --print-architecture
OUTPUT_VARIABLE CPACK_DEBIAN_PACKAGE_ARCHITECTURE
OUTPUT_STRIP_TRAILING_WHITESPACE)
execute_process(COMMAND lsb_release --codename --short
OUTPUT_VARIABLE DEBIAN_CODENAME
OUTPUT_STRIP_TRAILING_WHITESPACE)
if ("${DEBIAN_CODENAME}" STREQUAL "n/a")
set(DEBIAN_CODENAME "unknown")
endif()
if (WITH_RPM)
set(CPACK_GENERATOR "RPM")
set(CPACK_RPM_PACKAGE_LICENSE "GPL-3")
set(CPACK_RPM_PACKAGE_URL "https://schlomp.space/tastytea/${CMAKE_PROJECT_NAME}")
set(CPACK_RPM_PACKAGE_REQUIRES "cryptopp++, libMagick++, libxdg-basedir >= 1.2.0")
set(CPACK_PACKAGE_FILE_NAME
"${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}-0.${CPACK_PACKAGE_ARCHITECTURE}")
# The default does not produce valid Debian package names.
set(CPACK_DEBIAN_FILE_NAME
"${CPACK_PACKAGE_NAME}_${CPACK_PACKAGE_VERSION}-0_${CPACK_DEBIAN_PACKAGE_ARCHITECTURE}_${DEBIAN_CODENAME}.deb")
# RPM
set(CPACK_RPM_PACKAGE_LICENSE "GPL-3")
# Figure out dependencies automatically.
set(CPACK_RPM_PACKAGE_AUTOREQ ON)
# Should be set automatically, but it is not.
execute_process(COMMAND uname -m
OUTPUT_VARIABLE CPACK_RPM_PACKAGE_ARCHITECTURE
OUTPUT_STRIP_TRAILING_WHITESPACE)
set(CPACK_PACKAGE_FILE_NAME
"${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}-0.${CPACK_RPM_PACKAGE_ARCHITECTURE}")
execute_process(COMMAND lsb_release --id --short
OUTPUT_VARIABLE OS
OUTPUT_STRIP_TRAILING_WHITESPACE)
if("${OS}" STREQUAL "openSUSE")
execute_process(COMMAND lsb_release --release --short
OUTPUT_VARIABLE OS_RELEASE
OUTPUT_STRIP_TRAILING_WHITESPACE)
set(CPACK_PACKAGE_FILE_NAME
"${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}-0.${CPACK_RPM_PACKAGE_ARCHITECTURE}.opensuse-${OS_RELEASE}")
endif()
include(CPack)

View File

@ -22,12 +22,14 @@ server {
fastcgi_pass unix:/var/run/cgi-fcgiwrap.socket-1;
fastcgi_param SCRIPT_FILENAME /usr/bin/libravatarserv;
# fastcgi_param LIBRAVATARSERV_DIR "/var/db/libravatarserv"
# fastcgi_param LIBRAVATARSERV_DEFAULT_FALLBACK 404;
# fastcgi_param LIBRAVATARSERV_REDIRECT_NOFALLBACK 0;
# fastcgi_param LIBRAVATARSERV_REDIRECT_NOUSER 0;
# fastcgi_param LIBRAVATARSERV_REDIRECT 1;
fastcgi_cache avatar;
fastcgi_cache_valid 200 2h; # Cache answers for up to 2 hours.
fastcgi_cache_lock on; # Relay only one identical request at a time.
fastcgi_cache_valid 200 307 400 501 1h; # Cache answers for up to 1 hour
# Cache negative replies that may be temporary for 10 minutes
fastcgi_cache_valid 404 500 10m;
fastcgi_cache_lock on; # Relay only one identical request at a time.
}
}

20
src/CMakeLists.txt Normal file
View File

@ -0,0 +1,20 @@
include(GNUInstallDirs)
configure_file("version.hpp.in" "version.hpp" @ONLY)
configure_file("fs-compat.hpp.in" "fs-compat.hpp" @ONLY)
file(GLOB sources_src CONFIGURE_DEPENDS *.cpp)
file(GLOB headers_src CONFIGURE_DEPENDS *.hpp)
add_executable(${PROJECT_NAME})
target_sources(${PROJECT_NAME} PRIVATE "${sources_src}" "${headers_src}")
target_include_directories(${PROJECT_NAME}
PRIVATE "$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>")
target_link_libraries(${PROJECT_NAME}
PRIVATE
PkgConfig::Magick++
PkgConfig::libcryptopp
std::filesystem
identiconpp::identiconpp)
install(TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR})

33
src/config.cpp Normal file
View File

@ -0,0 +1,33 @@
/* This file is part of libravatarserv.
* Copyright © 2022 tastytea <tastytea@tastytea.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "config.hpp"
#include "fs-compat.hpp"
#include "helpers.hpp"
namespace libravatarserv::config {
config read() {
config cfg;
cfg.avatar_dir = read_env("LIBRAVATARSERV_DIR", fs::current_path().c_str());
cfg.default_fallback = read_env("LIBRAVATARSERV_DEFAULT_FALLBACK", "404");
cfg.redirect = (read_env("LIBRAVATARSERV_REDIRECT") == "1");
return cfg;
}
} // namespace libravatarserv::config

36
src/config.hpp Normal file
View File

@ -0,0 +1,36 @@
/* This file is part of libravatarserv.
* Copyright © 2022 tastytea <tastytea@tastytea.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef LIBRAVATARSERV_CONFIG_HPP
#define LIBRAVATARSERV_CONFIG_HPP
#include "fs-compat.hpp"
#include <string>
namespace libravatarserv::config {
struct config {
fs::path avatar_dir;
std::string default_fallback;
bool redirect;
};
[[nodiscard]] config read();
} // namespace libravatarserv::config
#endif // LIBRAVATARSERV_CONFIG_HPP

View File

@ -1,5 +1,5 @@
/* This file is part of libravatarserv.
* Copyright © 2018, 2020 tastytea <tastytea@tastytea.de>
* Copyright © 2022 tastytea <tastytea@tastytea.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,90 +14,74 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "libravatarserv.hpp"
#include "hash.hpp"
#define CRYPTOPP_ENABLE_NAMESPACE_WEAK 1
#include "types.hpp"
#include <algorithm>
#include <memory>
#include <string>
#include <string_view>
#define CRYPTOPP_ENABLE_NAMESPACE_WEAK 1 // necessary for MD5
#include <cryptopp/cryptlib.h>
#include <cryptopp/filters.h>
#include <cryptopp/hex.h>
#include <cryptopp/md5.h>
#include <cryptopp/sha.h>
#include <algorithm>
#include <string>
namespace libravatarserv::hash {
using std::string;
using namespace libravatarserv;
using namespace libravatarserv::hash;
const string hash::md5(const string &text)
{
std::string generate_hash(const algorithm_t algo, const std::string_view text) {
using namespace CryptoPP;
Weak::MD5 hash;
string digest;
StringSource s(
text, true,
new HashFilter(hash, new HexEncoder(new StringSink(digest))));
std::transform(digest.begin(), digest.end(), digest.begin(), ::tolower);
return digest;
SHA256 sha256;
Weak::MD5 md5;
HashTransformation *algo_to_use{&sha256}; // HashFilter calls free()?
if (algo == algorithm_t::MD5) {
algo_to_use = &md5;
}
std::string hash;
StringSource s(text.data(), true,
new HashFilter(*algo_to_use,
new HexEncoder(new StringSink(hash))));
std::transform(hash.begin(), hash.end(), hash.begin(), ::tolower);
return hash;
}
const string hash::sha256(const string &text)
{
using namespace CryptoPP;
SHA256 hash;
string digest;
StringSource s(
text, true,
new HashFilter(hash, new HexEncoder(new StringSink(digest))));
std::transform(digest.begin(), digest.end(), digest.begin(), ::tolower);
return digest;
}
bool hash::fill_table()
{
for (const fs::path &path :
fs::recursive_directory_iterator(settings::avatar_dir))
{
if (fs::is_regular_file(path))
{
string email = path.filename();
std::string find_email(const request_t &req, const fs::path &directory) {
bool default_present{false};
for (const fs::path &path : fs::recursive_directory_iterator(directory)) {
if (fs::is_regular_file(path)) {
std::string email = path.filename();
std::transform(email.begin(), email.end(), email.begin(),
::tolower);
table.insert({md5(email), email});
table.insert({sha256(email), email});
if (generate_hash(req.algo, email) == req.hash) {
return email;
}
if (email == "default") {
default_present = true;
}
}
}
return true;
return default_present ? "default" : "";
}
bool hash::is_valid(const string &digest)
{
if (digest.length() != 64 && digest.length() != 32)
{
return false;
}
if (std::any_of(digest.begin(), digest.end(), not_hex))
{
return false;
}
bool is_valid(const std::string_view hash) {
auto not_hex{[](const char c) {
if (c >= 0x61 && c <= 0x66) { // a-f
return false;
}
if (c >= 0x30 && c <= 0x39) { // 0-9
return false;
}
return true;
}};
return true;
return !std::any_of(hash.begin(), hash.end(), not_hex);
}
bool hash::not_hex(const char &c)
{
if (c >= 0x61 && c <= 0x66)
{ // a-f
return false;
}
if (c >= 0x30 && c <= 0x39)
{ // 0-9
return false;
}
return true;
}
} // namespace libravatarserv::hash

41
src/hash.hpp Normal file
View File

@ -0,0 +1,41 @@
/* This file is part of libravatarserv.
* Copyright © 2022 tastytea <tastytea@tastytea.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef LIBRAVATARSERV_HASH_HPP
#define LIBRAVATARSERV_HASH_HPP
#include "fs-compat.hpp"
#include "types.hpp"
#include <string>
#include <string_view>
namespace libravatarserv::hash {
// generate hash from text
[[nodiscard]] std::string generate_hash(algorithm_t algo,
std::string_view text);
// Match hash to email
[[nodiscard]] std::string find_email(const request_t &req,
const fs::path &directory);
// return false if invalid characters are found in hash
[[nodiscard]] bool is_valid(std::string_view hash);
} // namespace libravatarserv::hash
#endif // LIBRAVATARSERV_HASH_HPP

34
src/helpers.cpp Normal file
View File

@ -0,0 +1,34 @@
/* This file is part of libravatarserv.
* Copyright © 2022 tastytea <tastytea@tastytea.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "helpers.hpp"
#include <cstdlib>
namespace libravatarserv {
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
std::string_view read_env(const std::string_view name,
const std::string_view default_value) {
// NOLINTNEXTLINE(concurrency-mt-unsave)
const char *env{std::getenv(name.data())};
if (env != nullptr) {
return env;
}
return default_value;
}
} // namespace libravatarserv

28
src/helpers.hpp Normal file
View File

@ -0,0 +1,28 @@
/* This file is part of libravatarserv.
* Copyright © 2022 tastytea <tastytea@tastytea.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef LIBRAVATARSERV_HELPERS_HPP
#define LIBRAVATARSERV_HELPERS_HPP
#include <string_view>
namespace libravatarserv {
[[nodiscard]] std::string_view read_env(std::string_view name,
std::string_view default_value = "");
} // namespace libravatarserv
#endif // LIBRAVATARSERV_HELPERS_HPP

View File

@ -1,145 +0,0 @@
/* This file is part of libravatarserv.
* Copyright © 2018,2019,2020 tastytea <tastytea@tastytea.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "libravatarserv.hpp"
#include <algorithm>
#include <cstdlib>
#include <iostream>
using std::cerr;
using std::cout;
using std::endl;
using namespace libravatarserv;
using namespace libravatarserv::http;
const Request http::parse_request(const string &request)
{
if (request.substr(0, 8) == "/favicon")
{
cout << "Status: 404 Not Found\n\n";
std::exit(1);
}
if (request.substr(0, 8) != "/avatar/"
|| request.find("..", 8) != std::string::npos)
{
cout << "Status: 400 Bad Request\n\n";
cerr << "Error: Invalid URL.\n";
std::exit(1);
}
int16_t size = 80;
string fallback;
string digest = request.substr(8);
std::transform(digest.begin(), digest.end(), digest.begin(), ::tolower);
std::size_t pos_digest = digest.find('?');
if (pos_digest != std::string::npos)
{
string answer;
{
answer = get_parameter(request, "s");
if (!answer.empty())
{
try
{
size = static_cast<int16_t>(std::stoul(answer));
}
catch (const std::exception &)
{}
}
else
{
answer = get_parameter(request, "size");
if (!answer.empty())
{
try
{
size = static_cast<int16_t>(std::stoul(answer));
}
catch (const std::exception &)
{}
}
}
if (size > 512)
{
size = 512;
}
else if (size <= 0)
{
size = 80;
}
}
{
answer = get_parameter(request, "d");
if (!answer.empty())
{
fallback = answer;
}
else
{
answer = get_parameter(request, "default");
if (!answer.empty())
{
fallback = answer;
}
}
}
}
digest = digest.substr(0, pos_digest);
pos_digest = digest.find('.');
if (pos_digest != std::string::npos)
{
digest = digest.substr(0, pos_digest);
}
return {digest, static_cast<uint16_t>(size), fallback};
}
const string http::get_parameter(const string &request, const string &parameter)
{
std::size_t pos = request.find("&" + parameter + "=");
if (pos == std::string::npos)
{
pos = request.find("?" + parameter + "=");
}
if (pos != std::string::npos)
{
pos += (2 + parameter.length());
return request.substr(pos, request.find('&', pos));
}
return "";
}
void http::send_redirect(const Request &request)
{
const char *env = std::getenv("HTTPS");
string baseurl;
if (env != nullptr && env[1] == 'n')
{ // "on"
baseurl = "https://seccdn.libravatar.org";
}
else
{
baseurl = "http://cdn.libravatar.org";
}
cout << "Status: 307 Temporary Redirect\n";
cout << "Location: " << baseurl << "/avatar/" << request.digest
<< "?s=" << request.size << "&d=" << request.fallback << endl
<< endl;
}

View File

@ -1,5 +1,5 @@
/* This file is part of libravatarserv.
* Copyright © 2018, 2020 tastytea <tastytea@tastytea.de>
* Copyright © 2022 tastytea <tastytea@tastytea.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,73 +14,90 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "libravatarserv.hpp"
#include "image.hpp"
#include <Magick++/Color.h>
#include <Magick++/Exception.h>
#include "types.hpp"
#include <identiconpp.hpp>
#include <Magick++/Blob.h>
#include <Magick++/Geometry.h>
#include <Magick++/Image.h>
#include <algorithm>
#include <array>
#include <cstdint>
#include <ios>
#include <iostream>
#include <sstream>
#include <string>
#include <string_view>
using std::cerr;
using std::cout;
using std::endl;
using namespace libravatarserv;
using namespace libravatarserv::image;
namespace libravatarserv::image {
const Image image::get(const string &digest, const uint16_t size)
{
uint8_t error = 0;
Magick::Image get(const fs::path &filepath, const size_t size) {
Magick::Image img;
fs::path filename;
const auto &entry = hash::table.find(digest);
if (entry == hash::table.end())
{
filename = settings::avatar_dir / "default";
if (!fs::is_regular_file(filename))
{
cerr << "Warning: User not found and no default image set.\n";
return {2, img};
}
}
else
{
filename = settings::avatar_dir / entry->second;
}
try
{
img.read(filename);
img.resize(Magick::Geometry(size, size));
}
catch (const Magick::Exception &e)
{
cerr << "Error: " << e.what() << endl;
error = 2;
}
catch (const std::exception &e)
{
cerr << "Error: " << e.what() << endl;
error = 5;
}
return {error, img};
img.read(filepath);
img.resize(Magick::Geometry(size, size));
return img;
}
void image::write(Image &image)
{
string magick = image.image.magick();
Magick::Image generate_identicon(const request_t &req) {
const auto padwidth{[&req]() -> std::uint8_t {
if (req.size < 60) {
return 0;
}
return static_cast<std::uint8_t>(std::max(req.size / 10, 10));
}()};
Identiconpp identicon(5, 5, Identiconpp::algorithm::sigil, "fefefeff",
{
// The same colors ivatar uses.
"2d4fffff", // Blue
"feb42cff", // Yellow
"e279eaff", // Bright pink
"1eb3fdff", // Cyan
"E84D41ff", // Red
"31CB73ff", // Green
"8D45AAff" // Dark pink
},
{padwidth, padwidth});
Magick::Image img{
identicon.generate(req.hash, static_cast<std::uint8_t>(req.size))};
img.magick("PNG");
return img;
}
void write(Magick::Image &img,
const std::optional<std::string_view> magick_force) {
std::string magick{magick_force.value_or(img.magick())};
std::transform(magick.begin(), magick.end(), magick.begin(), ::tolower);
Magick::Blob res_buffer;
image.image.magick(magick); // force the same format
image.image.write(&res_buffer);
const auto quality{[&img, &magick]() -> size_t {
if (img.quality() != 0) {
return img.quality(); // same as source
}
if (magick == "png") {
return 100; // highest compression
}
if (magick == "webp") {
if (img.magick() == "WEBP" || img.magick() == "PNG"
|| img.magick() == "GIF" || img.magick() == "SVG") {
return 100; // lossless
}
}
return 0; // default
}()};
cout << "Content-Type: image/" << magick << endl;
cout << "Content-Length: " << res_buffer.length() << endl << endl;
cout.write(static_cast<const char *>(res_buffer.data()),
res_buffer.length());
img.magick(magick);
img.quality(quality);
Magick::Blob buffer;
img.write(&buffer);
std::cout << "Content-Type: image/" << magick << '\n';
std::cout << "Content-Length: " << buffer.length() << "\n\n";
std::cout.write(static_cast<const char *>(buffer.data()),
static_cast<std::streamsize>(buffer.length()));
}
} // namespace libravatarserv::image

41
src/image.hpp Normal file
View File

@ -0,0 +1,41 @@
/* This file is part of libravatarserv.
* Copyright © 2022 tastytea <tastytea@tastytea.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef LIBRAVATARSERV_IMAGE_HPP
#define LIBRAVATARSERV_IMAGE_HPP
#include "fs-compat.hpp"
#include "types.hpp"
#include <Magick++/Image.h>
#include <optional>
namespace libravatarserv::image {
// read image from file
[[nodiscard]] Magick::Image get(const fs::path &filepath, size_t size);
// Generate identicon from hash
[[nodiscard]] Magick::Image generate_identicon(const request_t &req);
// write image with headers to stdout
void write(Magick::Image &img,
std::optional<std::string_view> magick_force = {});
} // namespace libravatarserv::image
#endif // LIBRAVATARSERV_IMAGE_HPP

View File

@ -1,159 +0,0 @@
/* This file is part of libravatarserv.
* Copyright © 2018, 2019, 2020 tastytea <tastytea@tastytea.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "libravatarserv.hpp"
#include "version.hpp"
#include <Magick++/Geometry.h>
#include <identiconpp.hpp>
#include <iostream>
using namespace libravatarserv;
using std::cerr;
using std::cout;
using std::endl;
// Global variables
std::map<const string, const string> hash::table;
fs::path settings::avatar_dir;
settings::Settings settings::settings;
int main()
{
cout << "Server: libravatarserv/" << global::version << endl;
const char *request = std::getenv("REQUEST_URI");
if (request == nullptr)
{
cout << "Status: 400 Bad Request\n\n";
cerr << "Error: ${REQUEST_URI} is empty.\n";
return 1;
}
http::Request avatar = http::parse_request(request);
if (!hash::is_valid(avatar.digest))
{
cout << "Status: 400 Bad Request\n\n";
cerr << "Error: Hash is invalid\n";
return 1;
}
if (!settings::find_avatar_dir())
{
cout << "Status: 503 Service Unavailable\n\n";
cerr << "Error: No avatars found.\n";
return 3;
}
hash::fill_table();
settings::read_settings();
image::Image image = image::get(avatar.digest, avatar.size);
if (image.error == 0)
{
image::write(image);
}
else
{
cerr << "Warning " << std::to_string(image.error)
<< ": Could not open file.\n";
if (settings::settings.redirect_nouser)
{
http::send_redirect(avatar);
return 0;
}
if (avatar.fallback.empty())
{
avatar.fallback = settings::settings.default_fallback;
}
if (avatar.fallback.substr(0, 3) == "404")
{
cout << "Status: 404 Not Found\n\n";
}
else if (avatar.fallback.substr(0, 4) == "http")
{
cout << "Status: 307 Temporary Redirect\n";
cout << "Location: " << avatar.fallback << endl << endl;
}
else if (avatar.fallback.substr(0, 2) == "mp"
|| avatar.fallback.substr(0, 2) == "mm")
{
// MD5 hash of 'mp'
image = image::get("1f2dfa567dcf95833eddf7aec167fec7", avatar.size);
if (image.error == 0)
{
image::write(image);
}
else
{
cerr << "Error: Mystery person not found.\n";
// Jump to last else
goto not_implemented;
}
}
else if (avatar.fallback.substr(0, 9) == "identicon"
|| avatar.fallback.substr(0, 5) == "retro")
{
try
{
auto padwidth = static_cast<uint8_t>(avatar.size / 10);
if (avatar.size < 60)
{
padwidth = 0;
}
else if (padwidth < 10)
{
padwidth = 10;
}
Identiconpp identicon(5, 5, Identiconpp::algorithm::sigil,
"fefefeff",
{
// The same colors ivatar uses.
"2d4fffff", // Blue
"feb42cff", // Yellow
"e279eaff", // Bright pink
"1eb3fdff", // Cyan
"E84D41ff", // Red
"31CB73ff", // Green
"8D45AAff" // Dark pink
},
{padwidth, padwidth});
image.image = identicon.generate(avatar.digest, avatar.size);
image.image.magick("PNG");
image::write(image);
}
catch (const std::exception &e)
{
cout << "Status: 500 Internal Server Error\n\n";
cerr << "Error: Couldn't generate identicon (" << e.what()
<< ").\n";
}
}
else
{
not_implemented:
if (settings::settings.redirect_nofallback)
{
http::send_redirect(avatar);
}
else
{
cout << "Status: 501 Not Implemented\n\n";
}
}
}
return 0;
}

View File

@ -1,90 +0,0 @@
/* This file is part of libravatarserv.
* Copyright © 2018, 2020 tastytea <tastytea@tastytea.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef LIBRAVATARSERV_HPP
#define LIBRAVATARSERV_HPP
#include "fs-compat.hpp"
#include <Magick++/Image.h>
#include <map>
#include <string>
namespace libravatarserv
{
using std::int8_t;
using std::string;
using std::uint16_t;
using std::uint8_t;
int main();
namespace settings
{ // settings.cpp
extern fs::path avatar_dir;
extern struct Settings
{
string default_fallback = "404";
bool redirect_nofallback = false;
bool redirect_nouser = false;
} settings;
bool find_avatar_dir();
void read_settings();
} // namespace settings
namespace http // http.cpp
{
struct Request
{
const string digest;
const uint16_t size = 0;
string fallback;
};
const Request parse_request(const string &request);
const string get_parameter(const string &request, const string &parameter);
void send_redirect(const Request &request);
} // namespace http
namespace hash // hash.cpp
{
extern std::map<const string, const string> table;
const string md5(const string &text);
const string sha256(const string &text);
bool fill_table();
bool is_valid(const string &digest);
bool not_hex(const char &c);
} // namespace hash
namespace image // image.cpp
{
struct Image
{
uint8_t error = 0;
Magick::Image image;
};
const Image get(const string &digest, uint16_t size);
void write(Image &image);
Image identicon(const string &digest);
} // namespace image
} // namespace libravatarserv
#endif // LIBRAVATARSERV_HPP

108
src/main.cpp Normal file
View File

@ -0,0 +1,108 @@
/* This file is part of libravatarserv.
* Copyright © 2022 tastytea <tastytea@tastytea.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "config.hpp"
#include "fs-compat.hpp"
#include "hash.hpp"
#include "helpers.hpp"
#include "image.hpp"
#include "request.hpp"
#include "version.hpp"
#include <Magick++/Exception.h>
#include <Magick++/Image.h>
#include <cstring>
#include <iostream>
#include <stdexcept>
#include <string>
int main(int /*argc*/, char * /*argv*/[]) {
using namespace libravatarserv;
std::cout << "Server: libravatarserv/" << version << '\n';
try {
const std::string uri{read_env("REQUEST_URI")};
if (uri.empty()) {
std::cout << "Status: 400 Bad Request\n\n";
throw std::runtime_error{"REQUEST_URI is empty"};
}
auto cfg{config::read()};
auto req{request::parse(uri)};
std::string email{hash::find_email(req, cfg.avatar_dir)};
Magick::Image img;
if (!email.empty()) {
try {
img = image::get(cfg.avatar_dir / email,
static_cast<size_t>(req.size));
} catch (const Magick::Exception &e) {
std::cout << "Status: 500 Internal Server Error\n\n";
std::cerr << "ERROR: " << e.what() << '\n';
return 1;
}
} else {
if (cfg.redirect) {
std::cout << "Status: 307 Temporary Redirect\n";
std::cout << "Location: https://seccdn.libravatar.org/avatar/"
<< req.hash << "?s=" << req.size
<< "&d=" << req.fallback << "\n\n";
return 0;
}
if (req.fallback.empty()) {
req.fallback = cfg.default_fallback;
}
if (req.fallback == "404") {
std::cout << "Status: 404 Not Found\n\n";
return 0;
}
if (req.fallback == "mp" || req.fallback == "mm") {
if (!fs::exists(cfg.avatar_dir / "mp")) {
std::cout << "Status: 404 Not Found\n\n";
return 0;
}
email = "mp";
} else if (req.fallback == "identicon" || req.fallback == "retro") {
try {
img = image::generate_identicon(req);
} catch (const std::exception &e) {
std::cout << "Status: 500 Internal Server Error\n\n";
std::cerr
<< "ERROR: could not generate identicon: " << e.what()
<< "\n";
return 1;
}
} else {
std::cout << "Status: 501 Not Implemented\n\n";
}
}
image::write(img, req.magick);
} catch (const std::runtime_error &e) {
if (std::strlen(e.what()) != 0) {
std::cerr << "ERROR: " << e.what() << '\n';
}
return 1;
}
return 0;
}

122
src/request.cpp Normal file
View File

@ -0,0 +1,122 @@
/* This file is part of libravatarserv.
* Copyright © 2022 tastytea <tastytea@tastytea.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "request.hpp"
#include "hash.hpp"
#include "types.hpp"
#include <algorithm>
#include <array>
#include <cstdint>
#include <iostream>
#include <stdexcept>
#include <string>
#include <string_view>
namespace libravatarserv::request {
request_t parse(const std::string_view uri) {
constexpr std::uint8_t offset{8};
if (uri.substr(0, offset) != "/avatar/"
|| uri.find("..", offset) != std::string_view::npos) {
std::cout << "Status: 400 Bad Request\n\n";
throw std::runtime_error{"Invalid URL"};
}
request_t req;
req.size = 80;
size_t pos{uri.find('?')};
if (pos == std::string_view::npos) {
pos = uri.size();
}
req.hash.resize(pos - offset);
std::transform(uri.begin() + offset, uri.begin() + pos, req.hash.begin(),
::tolower);
if (const auto pos_dot{req.hash.find('.')}; pos_dot != std::string::npos) {
std::string magick{req.hash.substr(pos_dot + 1)};
req.hash = req.hash.substr(0, pos_dot);
if (magick == "jpg") {
magick = "jpeg";
}
constexpr std::array<std::string_view, 4> allowed{
{"jpeg", "png", "gif", "webp"}
};
if (std::any_of(allowed.begin(), allowed.end(),
[magick](const std::string_view supplied) {
return magick == supplied;
})) {
req.magick = magick;
}
}
if (req.hash.size() == 64 && hash::is_valid(req.hash)) {
req.algo = algorithm_t::SHA256;
} else if (req.hash.size() == 32 && hash::is_valid(req.hash)) {
req.algo = algorithm_t::MD5;
} else {
std::cout << "Status: 400 Bad Request\n\n";
throw std::runtime_error{"Invalid hash"};
}
if (pos != uri.size()) {
std::string uri_rest;
uri_rest.resize(uri.size() - pos);
std::transform(uri.begin() + pos, uri.end(), uri_rest.begin(),
::tolower);
auto strsize{get_parameter(uri_rest, "s")};
if (strsize.empty()) {
strsize = get_parameter(uri_rest, "size");
}
if (!strsize.empty()) {
try {
req.size = static_cast<int16_t>(std::stoul(strsize.data()));
if (req.size > 512) {
req.size = 512;
} else if (req.size < 1) {
req.size = 80;
}
} catch (...) {}
}
req.fallback = get_parameter(uri_rest, "d");
if (req.fallback.empty()) {
req.fallback = get_parameter(uri_rest, "default");
}
}
return req;
}
std::string_view get_parameter(const std::string_view uri,
const std::string &parameter) {
size_t pos{uri.find("?" + parameter + "=")};
if (pos == std::string_view::npos) {
pos = uri.find("&" + parameter + "=");
}
if (pos != std::string_view::npos) {
pos += (2 + parameter.length());
return uri.substr(pos, uri.find('&', pos));
}
return {};
}
} // namespace libravatarserv::request

36
src/request.hpp Normal file
View File

@ -0,0 +1,36 @@
/* This file is part of libravatarserv.
* Copyright © 2022 tastytea <tastytea@tastytea.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef LIBRAVATARSERV_REQUEST_HPP
#define LIBRAVATARSERV_REQUEST_HPP
#include "types.hpp"
#include <string>
#include <string_view>
namespace libravatarserv::request {
// parse request and return results
[[nodiscard]] request_t parse(std::string_view uri);
// get one parameter from an URI
[[nodiscard]] std::string_view get_parameter(std::string_view uri,
const std::string &parameter);
} // namespace libravatarserv::request
#endif // LIBRAVATARSERV_REQUEST_HPP

View File

@ -1,84 +0,0 @@
/* This file is part of libravatarserv.
* Copyright © 2018, 2020 tastytea <tastytea@tastytea.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "libravatarserv.hpp"
#include <basedir.h>
#include <cstdlib>
using namespace libravatarserv;
using namespace libravatarserv::settings;
bool settings::find_avatar_dir()
{
const char *envdir = std::getenv("LIBRAVATARSERV_DIR");
if (envdir == nullptr)
{
// TODO: Remove AVATAR_DIR in 1.0.0
// DEPRECATED because it is potentially used by another program
envdir = std::getenv("AVATAR_DIR");
}
if (envdir != nullptr && fs::is_directory(envdir))
{
avatar_dir = envdir;
}
else
{
xdgHandle xdg;
xdgInitHandle(&xdg);
auto data_dirs = xdgDataDirectories(&xdg);
const uint8_t size = sizeof(data_dirs) / sizeof(*data_dirs);
for (uint8_t index = 0; index <= size; ++index)
{
const string searchdir =
data_dirs[index] + string("/libravatarserv");
if (fs::is_directory(searchdir))
{
avatar_dir = searchdir;
break;
}
}
xdgWipeHandle(&xdg);
if (avatar_dir.empty())
{
return false;
}
}
return true;
}
void settings::read_settings()
{
const char *env = std::getenv("LIBRAVATARSERV_DEFAULT_FALLBACK");
if (env != nullptr)
{
settings.default_fallback = env;
}
env = std::getenv("LIBRAVATARSERV_REDIRECT_NOFALLBACK");
if (env != nullptr && env[0] == '1')
{
settings.redirect_nofallback = true;
}
env = std::getenv("LIBRAVATARSERV_REDIRECT_NOUSER");
if (env != nullptr && env[0] == '1')
{
settings.redirect_nouser = true;
}
}

40
src/types.hpp Normal file
View File

@ -0,0 +1,40 @@
/* This file is part of libravatarserv.
* Copyright © 2022 tastytea <tastytea@tastytea.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef LIBRAVATARSERV_TYPES_HPP
#define LIBRAVATARSERV_TYPES_HPP
#include <cstdint>
#include <optional>
#include <string>
namespace libravatarserv {
enum class algorithm_t {
SHA256,
MD5
};
struct request_t {
std::string hash;
algorithm_t algo;
std::int16_t size;
std::string fallback;
std::optional<std::string> magick;
};
} // namespace libravatarserv
#endif // LIBRAVATARSERV_TYPES_HPP

View File

@ -1,9 +1,19 @@
#ifndef VERSION_HPP
#define VERSION_HPP
/*!
* @file
*
* @brief Version variables.
*/
namespace global
{
static constexpr char version[] = "@PROJECT_VERSION@";
} // namespace global
#ifndef LIBRAVATARSERV_VERSION_HPP
#define LIBRAVATARSERV_VERSION_HPP
#endif // VERSION_HPP
#include <string_view>
namespace libravatarserv {
//! The version of the program.
inline constexpr std::string_view version{"@PROJECT_VERSION@"};
} // namespace libravatarserv
#endif // LIBRAVATARSERV_VERSION_HPP