Skip to content

Commit

Permalink
Add access file
Browse files Browse the repository at this point in the history
  • Loading branch information
SimulPiscator committed Oct 8, 2023
1 parent 20afae1 commit b3fc1e9
Show file tree
Hide file tree
Showing 12 changed files with 343 additions and 23 deletions.
16 changes: 16 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ add_executable(${PROJECT_NAME}
web/httpserver.cpp
web/webpage.cpp
web/errorpage.cpp
web/accessfile.cpp
imageformats/imageencoder.cpp
imageformats/jpegencoder.cpp
imageformats/pdfencoder.cpp
Expand Down Expand Up @@ -111,6 +112,11 @@ install(FILES launchd/org.simulpiscator.airsaned.plist
DESTINATION /Library/LaunchDaemons
)

install(CODE "
if(NOT EXISTS /usr/local/etc/airsane/access.conf)
file(INSTALL ${CMAKE_SOURCE_DIR}/etc/access.conf DESTINATION /usr/local/etc/airsane)
endif()
")
install(CODE "
if(NOT EXISTS /usr/local/etc/airsane/ignore.conf)
file(INSTALL ${CMAKE_SOURCE_DIR}/etc/ignore.conf DESTINATION /usr/local/etc/airsane)
Expand Down Expand Up @@ -140,6 +146,11 @@ install(TARGETS ${PROJECT_NAME}
RUNTIME DESTINATION sbin
)

install(CODE "
if(NOT EXISTS ${CMAKE_INSTALL_PREFIX}/etc/airsane/access.conf)
file(INSTALL ${CMAKE_SOURCE_DIR}/etc/access.conf DESTINATION ${CMAKE_INSTALL_PREFIX}/etc/airsane)
endif()
")
install(CODE "
if(NOT EXISTS ${CMAKE_INSTALL_PREFIX}/etc/airsane/ignore.conf)
file(INSTALL ${CMAKE_SOURCE_DIR}/etc/ignore.conf DESTINATION ${CMAKE_INSTALL_PREFIX}/etc/airsane)
Expand Down Expand Up @@ -175,6 +186,11 @@ install(FILES systemd/airsaned.default
RENAME airsane
)

install(CODE "
if(NOT EXISTS /etc/airsane/access.conf)
file(INSTALL ${CMAKE_SOURCE_DIR}/etc/access.conf DESTINATION /etc/airsane)
endif()
")
install(CODE "
if(NOT EXISTS /etc/airsane/ignore.conf)
file(INSTALL ${CMAKE_SOURCE_DIR}/etc/ignore.conf DESTINATION /etc/airsane)
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,16 @@ matches.
The original purpose of the ignore list is to avoid loops with backends that auto-detect eSCL devices, but it may be used to suppress
any device from AirSane's list of published devices.

## Access File

If a file exists at the location for the access file (by default, `/etc/airsane/access.conf`), the file's content
will be used to implement access restriction.

Each non-comment line in the access file defines a rule to allow or deny access from a range of IP addresses.
A rule either begins with "allow" to allow access, or "deny" to deny access. Separated with white space follows
a single IP address, or a range of IP addresses in the form "192.168.0.0/16" where the number after the slash character
defines the number of nonzero bits in the mask used to compare addresses.

## Troubleshoot

* Compiling fails with error: "‘png_const_bytep’ does not name a type".
Expand Down Expand Up @@ -307,3 +317,9 @@ Most likely, the avahi-daemon package is not installed, or avahi-daemon is not r
# Change MaxConnections=1 to MaxConnections=2 and save
sudo systemctl restart scanbm.socket
```

* You are unable to connect to the AirSane web page, or to scan from a remote computer.
AirSane comes with a pre-configured access file that restricts access to local addresses. To disable this mechanism,
you may temporarily rename the access file at `/etc/airsane/access.conf` (or `/usr/local/etc/airsane/access.conf` on
FreeBSD), and restart the daemon. If access is possible then, consider to add the remote machine's IP address
to an "allow" clause in the access file, and enable it again.
20 changes: 20 additions & 0 deletions etc/access.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Configure access to the AirSane server by IP address.

# If this file is empty, access is allowed from anywhere.
# Each entry is either Deny or Allow (or a comment).
# Following the Deny or Allow is an IP address (ranges allowed).
# Entries are matched from top to bottom. When the first match is found,
# the result will be "Allow" if it is an Allow entry, and will be "Deny"
# if it is a Deny entry.

# If no match occurs, a "Deny" will be the result. As an exception to
# this rule, an empty list will always result in an "Allow".

allow 127.0.0.0/8
allow 10.0.0.0/8
allow 172.16.0.0/12
allow 192.168.0.0/16

allow ::1
allow fe80::/10
allow fec0::/10
1 change: 1 addition & 0 deletions https/systemd/airsaned.default
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ DISCLOSE_VERSION=true
LOCAL_SCANNERS_ONLY=true
RANDOM_PATHS=false
OPTIONS_FILE=/etc/airsane/options.conf
ACCESS_FILE=
IGNORE_LIST=/etc/airsane/ignore.conf

35 changes: 29 additions & 6 deletions server/server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "scanner.h"
#include "basic/uuid.h"
#include "zeroconf/hotplugnotifier.h"
#include "web/accessfile.h"

extern const char* GIT_COMMIT_HASH;
extern const char* GIT_BRANCH;
Expand Down Expand Up @@ -92,7 +93,7 @@ Server::Server(int argc, char** argv)
{
std::string port, interface, unixsocket, accesslog, hotplug, announce,
webinterface, resetoption, discloseversion, localonly, optionsfile,
ignorelist, randompaths, compatiblepath, debug, announcesecure;
ignorelist, accessfile, randompaths, compatiblepath, debug, announcesecure;
struct
{
const std::string name, def, info;
Expand Down Expand Up @@ -130,6 +131,14 @@ Server::Server(int argc, char** argv)
#endif
"location of device ignore list",
ignorelist },
{ "access-file",
#ifdef __FreeBSD__
"/usr/local/etc/airsane/access.conf",
#else
"/etc/airsane/access.conf",
#endif
"location of access file",
accessfile },
{ "debug", "false", "log debug information to stderr", debug },
};
for (auto& opt : options)
Expand Down Expand Up @@ -171,6 +180,7 @@ Server::Server(int argc, char** argv)
mDiscloseversion = (discloseversion == "true");
mLocalonly = (localonly == "true");
mOptionsfile = optionsfile;
mAccessfile = accessfile;
mIgnorelist = ignorelist;

uint16_t port_ = 0;
Expand Down Expand Up @@ -280,13 +290,21 @@ Server::run()
mScanners.push_back(ScannerEntry({ pScanner, pService }));
}
}
ok = true;
AccessFile accessfile(mAccessfile);
if (!accessfile.errors().empty()) {
std::clog << "errors in accessfile:\n" << accessfile.errors() << " terminating" << std::endl;
ok = false;
}
HttpServer::applyAccessFile(accessfile);

::clock_gettime(CLOCK_MONOTONIC, &t);
float t1 = 1.0 * t.tv_sec + 1e-9 * t.tv_nsec;
std::clog << "end time is " << t1 << std::endl;
mStartupTimeSeconds = t1 - t0;
std::clog << "startup took " << mStartupTimeSeconds << " secconds" << std::endl;

ok = HttpServer::run();
ok = ok && HttpServer::run();
mScanners.clear();
if (ok && terminationStatus() == SIGHUP) {
std::clog << "received SIGHUP, reloading" << std::endl;
Expand Down Expand Up @@ -500,10 +518,15 @@ Server::handleScannerRequest(ScannerList::value_type entry, const std::string& p
}
res = res.substr(1);
size_t pos = res.find('/');
if (pos > res.length() && request.method() == HttpServer::HTTP_DELETE && entry.pScanner->cancelJob(res)) {
response.setStatus(HttpServer::HTTP_OK);
response.send();
return;
if (pos > res.length()) {
if (request.method() == HttpServer::HTTP_DELETE && entry.pScanner->cancelJob(res)) {
response.setStatus(HttpServer::HTTP_OK);
response.send();
return;
} else {
HttpServer::onRequest(request, response);
return;
}
}
if (res.substr(pos) == "/NextDocument" && request.method() == HttpServer::HTTP_GET) {
auto job = entry.pScanner->getJob(res.substr(0, pos));
Expand Down
2 changes: 1 addition & 1 deletion server/server.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class Server : public HttpServer
std::filebuf mLogfile;
bool mAnnounce, mWebinterface, mResetoption, mDiscloseversion,
mLocalonly, mHotplug, mRandompaths, mCompatiblepath, mAnnouncesecure;
std::string mOptionsfile, mIgnorelist;
std::string mOptionsfile, mAccessfile, mIgnorelist;
float mStartupTimeSeconds;
bool mDoRun;
};
Expand Down
1 change: 1 addition & 0 deletions systemd/airsaned.default
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ LOCAL_SCANNERS_ONLY=true
RANDOM_PATHS=false
COMPATIBLE_PATH=true
OPTIONS_FILE=/etc/airsane/options.conf
ACCESS_FILE=/etc/airsane/access.conf
IGNORE_LIST=/etc/airsane/ignore.conf

2 changes: 1 addition & 1 deletion systemd/airsaned.service.in
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ After=avahi-daemon.service

[Service]
EnvironmentFile=-/etc/default/airsane
ExecStart=@CMAKE_INSTALL_FULL_BINDIR@/airsaned --interface=${INTERFACE} --listen-port=${LISTEN_PORT} --access-log=${ACCESS_LOG} --hotplug=${HOTPLUG} --mdns-announce=${MDNS_ANNOUNCE} --announce-secure=${ANNOUNCE_SECURE} --unix-socket=${UNIX_SOCKET} --web-interface=${WEB_INTERFACE} --random-paths=${RANDOM_PATHS} --compatible-path=${COMPATIBLE_PATH} --local-scanners-only=${LOCAL_SCANNERS_ONLY} --disclose-version=${DISCLOSE_VERSION} --reset-option=${RESET_OPTION} --options-file=${OPTIONS_FILE} --ignore-list=${IGNORE_LIST}
ExecStart=@CMAKE_INSTALL_FULL_BINDIR@/airsaned --interface=${INTERFACE} --listen-port=${LISTEN_PORT} --access-log=${ACCESS_LOG} --hotplug=${HOTPLUG} --mdns-announce=${MDNS_ANNOUNCE} --announce-secure=${ANNOUNCE_SECURE} --unix-socket=${UNIX_SOCKET} --web-interface=${WEB_INTERFACE} --random-paths=${RANDOM_PATHS} --compatible-path=${COMPATIBLE_PATH} --local-scanners-only=${LOCAL_SCANNERS_ONLY} --disclose-version=${DISCLOSE_VERSION} --reset-option=${RESET_OPTION} --options-file=${OPTIONS_FILE} --access-file=${ACCESS_FILE} --ignore-list=${IGNORE_LIST}
ExecReload=/bin/kill -HUP $MAINPID
ExecStartPre=/bin/sleep 15
User=saned
Expand Down
169 changes: 169 additions & 0 deletions web/accessfile.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/*
AirSane Imaging Daemon
Copyright (C) 2018-2023 Simul Piscator
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, either version 3 of the License, or
(at your option) any later version.
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 "accessfile.h"
#include <fstream>
#include <sstream>
#include <cstring>
#include <cassert>

namespace {
bool SetMaskBits(HttpServer::Sockaddr& mask, int bits)
{
int width = 0;
char* data = nullptr;
if (mask.sa.sa_family == AF_INET) {
width = 32;
data = reinterpret_cast<char*>(&mask.in.sin_addr);
}
else if (mask.sa.sa_family == AF_INET6) {
width = 128;
data = reinterpret_cast<char*>(&mask.in6.sin6_addr);
}

if (bits > width)
return false;
if (!data)
return false;

::memset(data, width / 8, 0);
for (int i = 0; i < bits; ++i) {
int byte = i / 8, bit = i % 8;
data[byte] |= (1 << bit);
}
return true;
}

bool MatchAddresses(const HttpServer::Sockaddr& inAddr1, const HttpServer::Sockaddr& inAddr2, const HttpServer::Sockaddr& inMask)
{
if (inAddr1.sa.sa_family != inAddr2.sa.sa_family)
return false;
assert(inAddr2.sa.sa_family == inMask.sa.sa_family);

int width = 0;
const char* data1 = nullptr, *data2 = nullptr, *maskdata = nullptr;
if (inMask.sa.sa_family == AF_INET) {
width = 32;
data1 = reinterpret_cast<const char*>(&inAddr1.in.sin_addr);
data2 = reinterpret_cast<const char*>(&inAddr2.in.sin_addr);
maskdata = reinterpret_cast<const char*>(&inMask.in.sin_addr);
}
else if (inMask.sa.sa_family == AF_INET6) {
width = 128;
data1 = reinterpret_cast<const char*>(&inAddr1.in6.sin6_addr);
data2 = reinterpret_cast<const char*>(&inAddr2.in6.sin6_addr);
maskdata = reinterpret_cast<const char*>(&inMask.in6.sin6_addr);
}
for (int i = 0; i < width / 8; ++i) {
char c1 = data1[i] & maskdata[i],
c2 = data2[i] & maskdata[i];
if (c1 != c2)
return false;
}
return true;
}
}

AccessFile::AccessFile(const std::string& path)
{
if (path.empty())
return;
std::ifstream file(path);
if (!file.is_open())
return;
std::string line;
while (std::getline(file, line)) {
while (!line.empty() && std::iswspace(line.front()))
line = line.substr(1);
while (!line.empty() && std::iswspace(line.back()))
line.pop_back();
if (line.empty())
continue;
if (line.front() == '#')
continue;
std::istringstream iss(line);
Entry entry;
if (!entry.parse(iss))
mErrors += "Illegal entry: " + line + "\n";
else
mEntries.push_back(entry);
}
}

const std::string& AccessFile::errors() const
{
return mErrors;
}

bool AccessFile::isAllowed(const HttpServer::Sockaddr& addr) const
{
if (mEntries.empty())
return true;
for (const auto& entry : mEntries) {
int result = entry.match(addr);
if (result == Entry::Allow)
return true;
if (result == Entry::Deny)
return false;
}
return false;
}

std::istream& AccessFile::Entry::parse(std::istream& is)
{
std::string kind, address;
is >> kind;
if (!::strcasecmp(kind.c_str(), "allow"))
mKind = Allow;
else if (!::strcasecmp(kind.c_str(), "deny"))
mKind = Deny;
else
is.setstate(std::ios::failbit);
std::getline(is >> std::ws, address);
int bits = -1;
size_t pos = address.find_last_of("/");
if (pos != std::string::npos) {
bits = ::atoi(address.substr(pos + 1).c_str());
address = address.substr(0, pos);
}
if (::inet_pton(AF_INET, address.c_str(), &mAddress.in.sin_addr)) {
if (bits == -1)
bits = 32;
mAddress.sa.sa_family = AF_INET;
mMask = mAddress;
SetMaskBits(mMask, bits);
}
else if (::inet_pton(AF_INET6, address.c_str(), &mAddress.in6.sin6_addr)) {
if (bits == -1)
bits = 128;
mAddress.sa.sa_family = AF_INET6;
mMask = mAddress;
SetMaskBits(mMask, bits);
}
else {
is.setstate(std::ios::failbit);
}
return is;
}

int AccessFile::Entry::match(const HttpServer::Sockaddr &addr) const
{
if (!MatchAddresses(addr, mAddress, mMask))
return NoMatch;
return mKind;
}
Loading

0 comments on commit b3fc1e9

Please sign in to comment.