Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTTPD output: Added support for HTTP Basic authentication #1968

Closed
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/mpdconf.example
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ input {
# bitrate "128" # do not define if quality is defined
# format "44100:16:1"
# max_clients "0" # optional 0=no limit
# password "secretPassword" # optional, enables HTTP Basic authentication with set password
#}
#
# An example of a pulseaudio output (streaming to a remote pulseaudio server)
Expand Down
5 changes: 5 additions & 0 deletions doc/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1032,6 +1032,11 @@ It is highly recommended to configure a fixed format, because a stream cannot sw
- The genre of the stream. Will be reflected in the `icy-genre` header of the stream.
* - **website URL**
- The website of the stream. Will be reflected in the `icy-url` header of the stream.
* - **password passwd**
- Requires HTTP Basic authentication with set password when set to non-empty string.
Username is ignored and can contain any printable characters except colon (':').
Be aware that the password is transmitted over unencrypted connection only base64 encoded, and is therefore visible to anyone listening to your traffic.


The `name` from the `audio_output` block that uses this output plugin will be reflected as the stream name in the `icy-name` header of the stream.

Expand Down
36 changes: 34 additions & 2 deletions src/output/plugins/httpd/HttpdClient.cxx
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright The Music Player Daemon Project

#include "config.h"
#include "HttpdClient.hxx"
#include "HttpdInternal.hxx"
#include "util/ASCII.hxx"
#include "util/AllocatedString.hxx"
#include "Page.hxx"
#include "IcyMetaDataServer.hxx"
#include "lib/crypto/Base64.hxx"
#include "net/SocketError.hxx"
#include "net/UniqueSocketDescriptor.hxx"
#include "util/SpanCast.hxx"
#include "util/StringCompare.hxx"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why include this header?

Copy link
Author

@Mrkvak Mrkvak Apr 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed leftover, sorry.
Actually no, it's to include StringAfterPrefixIgnoreCase

#include "util/StringSplit.hxx"
#include "Log.hxx"

#include <fmt/core.h>

#include <cassert>
#include <cstring>

#ifdef HAVE_BASE64
#include "lib/crypto/Base64.hxx"
#endif


HttpdClient::~HttpdClient() noexcept
{
if (IsDefined())
Expand All @@ -41,6 +50,10 @@ HttpdClient::BeginResponse() noexcept
{
assert(state != State::RESPONSE);

if (!head_method) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the head_method check?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's necessary to password protect HEAD requests, is it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You wrote extra code to implement a special case to allow HEAD requests. What justifies this extra complexity?
Your answer isn't good because extra complexity should be justified, not the other way round.

should_reject_unauthorized = httpd.password != nullptr && provided_password != httpd.password;
}

state = State::RESPONSE;
current_page = nullptr;

Expand Down Expand Up @@ -102,6 +115,15 @@ HttpdClient::HandleLine(const char *line) noexcept
return true;
}


#ifdef HAVE_BASE64
if (const char *b64 = StringAfterPrefixIgnoreCase(line, "Authorization: Basic ")) {
auto decodedBytes = DecodeBase64(b64);
auto [username, pw] = Split(ToStringView(decodedBytes), ':');
provided_password = std::string{pw};
}
#endif

if (StringEqualsCaseASCII(line, "Icy-MetaData: 1", 15) ||
StringEqualsCaseASCII(line, "Icy-MetaData:1", 14)) {
/* Send icy metadata */
Expand Down Expand Up @@ -132,6 +154,15 @@ HttpdClient::SendResponse() noexcept
"Connection: close\r\n"
"\r\n"
"404 not found";
} else if (should_reject_unauthorized) {
allocated = fmt::format(
"HTTP/1.1 401 unauthorized\r\n"
"Content-Type: text/plain\r\n"
"WWW-Authenticate: Basic realm=\"{}\"\r\n"
"Connection: close\r\n"
"\r\n"
"401 unauthorized", httpd.name);
response = allocated.c_str();
} else if (metadata_requested) {
allocated =
icy_server_metadata_header(httpd.name, httpd.genre,
Expand Down Expand Up @@ -169,7 +200,8 @@ HttpdClient::HttpdClient(HttpdOutput &_httpd, UniqueSocketDescriptor _fd,
bool _metadata_supported)
:BufferedSocket(_fd.Release(), _loop),
httpd(_httpd),
metadata_supported(_metadata_supported)
metadata_supported(_metadata_supported),
provided_password{""}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the point of this initializer?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's needed after the last simplification, sorry I missed it.

{
}

Expand Down Expand Up @@ -414,7 +446,7 @@ HttpdClient::OnSocketInput(void *data, size_t length) noexcept
if (!SendResponse())
return InputResult::CLOSED;

if (head_method || should_reject) {
if (head_method || should_reject || should_reject_unauthorized) {
LockClose();
return InputResult::CLOSED;
}
Expand Down
11 changes: 11 additions & 0 deletions src/output/plugins/httpd/HttpdClient.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ class HttpdClient final
*/
bool should_reject = false;

/**
* Should we reject this request as unauthorized?
*/
bool should_reject_unauthorized = false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This field should only exist when the feature is available



/* ICY */

/**
Expand Down Expand Up @@ -111,6 +117,11 @@ class HttpdClient final
*/
unsigned metadata_fill = 0;

/**
* Provided password (using HTTP basic auth)
*/
std::string provided_password;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This field should only exist when the feature is available


public:
/**
* @param httpd the HTTP output device
Expand Down
6 changes: 6 additions & 0 deletions src/output/plugins/httpd/HttpdInternal.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@ private:
*/
char const *const website;

/**
* The configured password.
*/
char const *password;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This field should only exist when the feature is available and it should be const



private:
/**
* A linked list containing all clients which are currently
Expand Down
2 changes: 2 additions & 0 deletions src/output/plugins/httpd/HttpdOutputPlugin.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ HttpdOutput::HttpdOutput(EventLoop &_loop, const ConfigBlock &block)
ServerSocket::SetDscpClass(value);
});

password = block.GetBlockValue("password");

/* set up bind_to_address */

ServerSocketAddGeneric(*this, block.GetBlockValue("bind_to_address"), block.GetBlockValue("port", 8000U));
Expand Down
2 changes: 1 addition & 1 deletion src/output/plugins/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ if get_option('httpd')
'httpd/HttpdClient.cxx',
'httpd/HttpdOutputPlugin.cxx',
]
output_plugins_deps += [ event_dep, net_dep ]
output_plugins_deps += [ event_dep, net_dep, crypto_base64_dep ]
need_encoder = true
endif

Expand Down