Basic oauth support
parent
93a521346b
commit
4a461de6bc
|
@ -28,9 +28,15 @@ class Extension(mopidy.ext.Extension):
|
|||
def get_config_schema(self):
|
||||
schema = super(Extension, self).get_config_schema()
|
||||
schema["url"] = mopidy.config.String()
|
||||
schema["authorization_endpoint"] = mopidy.config.String(optional=True)
|
||||
schema["token_endpoint"] = mopidy.config.String(optional=True)
|
||||
schema["client_secret"] = mopidy.config.String(optional=True)
|
||||
schema["client_id"] = mopidy.config.String(optional=True)
|
||||
|
||||
schema["username"] = mopidy.config.String(optional=True)
|
||||
schema["password"] = mopidy.config.Secret(optional=True)
|
||||
schema["cache_duration"] = mopidy.config.Integer(optional=True)
|
||||
schema["verify_cert"] = mopidy.config.Boolean(optional=True)
|
||||
return schema
|
||||
|
||||
def validate_config(self, config):
|
||||
|
@ -47,3 +53,8 @@ class Extension(mopidy.ext.Extension):
|
|||
from . import actor
|
||||
|
||||
registry.add("backend", actor.FunkwhaleBackend)
|
||||
|
||||
def get_command(self):
|
||||
from . import commands
|
||||
|
||||
return commands.FunkwhaleCommand()
|
||||
|
|
|
@ -24,7 +24,9 @@ class FunkwhaleBackend(pykka.ThreadingActor, backend.Backend):
|
|||
self.uri_schemes = ["funkwhale", "fw"]
|
||||
|
||||
def on_start(self):
|
||||
if self.client.username is not None:
|
||||
if self.config["funkwhale"]["client_id"]:
|
||||
logger.info('Using OAuth2 connection"')
|
||||
elif self.client.username is not None:
|
||||
self.client.login()
|
||||
logger.info(
|
||||
'Logged in to Funkwhale as "%s" on "%s"',
|
||||
|
@ -43,8 +45,12 @@ class FunkwhalePlaybackProvider(backend.PlaybackProvider):
|
|||
if track is None:
|
||||
return None
|
||||
url = track["listen_url"]
|
||||
|
||||
if url.startswith("/"):
|
||||
url = self.backend.config["funkwhale"]["url"] + url
|
||||
if self.backend.client.token:
|
||||
url += "?jwt=" + self.backend.client.token
|
||||
if self.backend.client.use_oauth:
|
||||
url += "?token=" + self.backend.client.oauth_token["access_token"]
|
||||
|
||||
elif self.backend.client.jwt_token:
|
||||
url += "?jwt=" + self.backend.client.jwt_token
|
||||
return url
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import requests
|
||||
import requests_oauthlib
|
||||
|
||||
from mopidy import httpclient, exceptions
|
||||
|
||||
|
@ -9,6 +12,8 @@ from . import Extension, __version__
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
REQUIRED_SCOPES = ["read:libraries", "read:favorites", "read:playlists"]
|
||||
|
||||
|
||||
class SessionWithUrlBase(requests.Session):
|
||||
# In Python 3 you could place `url_base` after `*args`, but not in Python 2.
|
||||
|
@ -28,7 +33,11 @@ class SessionWithUrlBase(requests.Session):
|
|||
return super(SessionWithUrlBase, self).request(method, modified_url, **kwargs)
|
||||
|
||||
|
||||
def get_requests_session(url, proxy_config, user_agent):
|
||||
class OAuth2Session(SessionWithUrlBase, requests_oauthlib.OAuth2Session):
|
||||
pass
|
||||
|
||||
|
||||
def get_requests_session(url, proxy_config, user_agent, base_cls, **kwargs):
|
||||
if not url.endswith("/"):
|
||||
url += "/"
|
||||
url += "api/v1/"
|
||||
|
@ -36,14 +45,14 @@ def get_requests_session(url, proxy_config, user_agent):
|
|||
proxy = httpclient.format_proxy(proxy_config)
|
||||
full_user_agent = httpclient.format_user_agent(user_agent)
|
||||
|
||||
session = SessionWithUrlBase(url_base=url)
|
||||
session = base_cls(url_base=url, **kwargs)
|
||||
session.proxies.update({"http": proxy, "https": proxy})
|
||||
session.headers.update({"user-agent": full_user_agent})
|
||||
|
||||
return session
|
||||
|
||||
|
||||
def login(session, username, password):
|
||||
def login_legacy(session, username, password):
|
||||
if not username:
|
||||
return
|
||||
response = session.post("token/", {"username": username, "password": password})
|
||||
|
@ -59,24 +68,53 @@ def login(session, username, password):
|
|||
class APIClient(object):
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.session = get_requests_session(
|
||||
config["funkwhale"]["url"],
|
||||
proxy_config=config["proxy"],
|
||||
user_agent="%s/%s" % (Extension.dist_name, __version__),
|
||||
)
|
||||
self.username = self.config["funkwhale"]["username"]
|
||||
self.token = None
|
||||
self.jwt_token = None
|
||||
self.oauth_token = get_token(config)
|
||||
if self.use_oauth:
|
||||
self.session = get_requests_session(
|
||||
config["funkwhale"]["url"],
|
||||
proxy_config=config["proxy"],
|
||||
user_agent="%s/%s" % (Extension.dist_name, __version__),
|
||||
base_cls=OAuth2Session,
|
||||
client_id=self.config["funkwhale"]["client_id"],
|
||||
token=self.oauth_token,
|
||||
auto_refresh_url=config["funkwhale"]["url"]
|
||||
+ config["funkwhale"].get("token_endpoint")
|
||||
or "/api/v1/oauth/token/",
|
||||
auto_refresh_kwargs={
|
||||
"client_id": self.config["funkwhale"]["client_id"],
|
||||
"client_secret": self.config["funkwhale"]["client_id"],
|
||||
},
|
||||
token_updater=self.refresh_token,
|
||||
)
|
||||
else:
|
||||
self.session = get_requests_session(
|
||||
config["funkwhale"]["url"],
|
||||
proxy_config=config["proxy"],
|
||||
user_agent="%s/%s" % (Extension.dist_name, __version__),
|
||||
base_cls=SessionWithUrlBase,
|
||||
)
|
||||
self.username = self.config["funkwhale"]["username"]
|
||||
self.session.verify = config["funkwhale"].get("verify_cert", True)
|
||||
|
||||
@property
|
||||
def use_oauth(self):
|
||||
return self.config["funkwhale"]["client_id"] and self.oauth_token
|
||||
|
||||
def refresh_token(self, token):
|
||||
self.oauth_token = token
|
||||
set_token(token, config)
|
||||
|
||||
def login(self):
|
||||
self.username = self.config["funkwhale"]["username"]
|
||||
if self.username:
|
||||
self.token = login(
|
||||
self.jwt_token = login_legacy(
|
||||
self.session,
|
||||
self.config["funkwhale"]["username"],
|
||||
self.config["funkwhale"]["password"],
|
||||
)
|
||||
else:
|
||||
self.token = None
|
||||
self.jwt_token = None
|
||||
|
||||
def search(self, query):
|
||||
response = self.session.get("search", params={"query": query})
|
||||
|
@ -120,3 +158,28 @@ class APIClient(object):
|
|||
next_page = payload.get("next")
|
||||
if max and counter >= max:
|
||||
next_page = None
|
||||
|
||||
|
||||
def get_token(config):
|
||||
import mopidy_funkwhale
|
||||
|
||||
data_dir = mopidy_funkwhale.Extension.get_data_dir(config)
|
||||
try:
|
||||
with open(os.path.join(data_dir, "token"), "r") as f:
|
||||
raw = f.read()
|
||||
except IOError:
|
||||
return None
|
||||
try:
|
||||
return json.loads(raw)
|
||||
except (TypeError, ValueError):
|
||||
logger.error("Cannot decode token data, you may need to relogin")
|
||||
|
||||
|
||||
def set_token(token_data, config):
|
||||
import mopidy_funkwhale
|
||||
|
||||
data_dir = mopidy_funkwhale.Extension.get_data_dir(config)
|
||||
print(data_dir)
|
||||
content = json.dumps(token_data)
|
||||
with open(os.path.join(data_dir, "token"), "w") as f:
|
||||
f.write(content)
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
from mopidy import commands, compat, exceptions
|
||||
|
||||
import requests_oauthlib
|
||||
|
||||
from . import client
|
||||
|
||||
|
||||
def urlencode(data):
|
||||
try:
|
||||
import urllib.parse
|
||||
|
||||
return urllib.parse.urlencode(data)
|
||||
except ImportError:
|
||||
# python 2
|
||||
import urllib
|
||||
|
||||
return urllib.urlencode(data)
|
||||
|
||||
|
||||
class FunkwhaleCommand(commands.Command):
|
||||
def __init__(self):
|
||||
super(FunkwhaleCommand, self).__init__()
|
||||
self.add_child("login", LoginCommand())
|
||||
|
||||
|
||||
class LoginCommand(commands.Command):
|
||||
help = (
|
||||
"Display authorization URL and instructions to connect with Funkwhale server."
|
||||
)
|
||||
|
||||
def run(self, args, config):
|
||||
import mopidy_funkwhale
|
||||
|
||||
url = config["funkwhale"]["url"]
|
||||
authorize_endpoint = (
|
||||
config["funkwhale"].get("authorize_endpoint") or "/authorize"
|
||||
)
|
||||
token_endpoint = (
|
||||
config["funkwhale"].get("token_endpoint") or "/api/v1/oauth/token/"
|
||||
)
|
||||
client_id = config["funkwhale"]["client_id"]
|
||||
client_secret = config["funkwhale"]["client_secret"]
|
||||
if not client_id or not client_secret:
|
||||
params = {
|
||||
"name": "Mopidy-Funkwhale",
|
||||
"scopes": " ".join(client.REQUIRED_SCOPES),
|
||||
"redirect_uris": "urn:ietf:wg:oauth:2.0:oob",
|
||||
}
|
||||
app_url = url + "/settings/applications/new?" + urlencode(params)
|
||||
print(
|
||||
"\nMissing client_id or client_secret! To connect to your Funkwhale account:\n\n"
|
||||
"1. Create an app by visiting {}"
|
||||
"\n2. Ensure the created app has 'urn:ietf:wg:oauth:2.0:oob' as "
|
||||
"redirect URI, and the following scopes: {}"
|
||||
"\n3. Update the client_id and client_secret values in the [funkwhale] section of your mopidy configuration, to match the values of the created application"
|
||||
"\n4. Relaunch this command".format(
|
||||
app_url, ", ".join(client.REQUIRED_SCOPES)
|
||||
)
|
||||
)
|
||||
return 1
|
||||
|
||||
oauth = requests_oauthlib.OAuth2Session(
|
||||
client_id,
|
||||
redirect_uri="urn:ietf:wg:oauth:2.0:oob",
|
||||
scope=client.REQUIRED_SCOPES,
|
||||
)
|
||||
oauth.verify = config["funkwhale"].get("verify_cert", True)
|
||||
authorize_url, state = oauth.authorization_url(url + authorize_endpoint)
|
||||
print(
|
||||
"\nTo login:\n\n"
|
||||
"1. Visit the following URL: {}"
|
||||
"\n2. Authorize the application"
|
||||
"\n3. Copy-paste the token you obtained and press enter".format(
|
||||
authorize_url
|
||||
)
|
||||
)
|
||||
|
||||
prompt = "\nEnter the token:"
|
||||
|
||||
authorization_code = compat.input(prompt)
|
||||
token = oauth.fetch_token(
|
||||
url + token_endpoint,
|
||||
code=authorization_code,
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
)
|
||||
client.set_token(token, config)
|
||||
print("Login successful!")
|
||||
return 0
|
|
@ -2,10 +2,28 @@
|
|||
enabled = true
|
||||
# URL of your funkwhale instance
|
||||
url = https://demo.funkwhale.audio
|
||||
|
||||
# Create an app by visiting <funkwhaleurl>/settings/applications/new
|
||||
# and past the client id and secret below. Use "urn:ietf:wg:oauth:2.0:oob"
|
||||
# as the app redirect URL, and request the following scopes:
|
||||
# - read:libraries
|
||||
client_id =
|
||||
client_secret =
|
||||
|
||||
# you don't need to update this
|
||||
authorization_endpoint = /authorize
|
||||
token_endpoint = /api/v1/oauth/token/
|
||||
|
||||
## Settings below are for legacy password-based auth, you should use OAuth instead
|
||||
|
||||
# Username to use when authenticating (leave empty fo anonymous access)
|
||||
username = demo
|
||||
username =
|
||||
# Password to use when authenticating (leave empty fo anonymous access)
|
||||
password = demo
|
||||
password =
|
||||
|
||||
# duration of cache entries before they are removed, in seconds
|
||||
# 0 to cache forever, empty to disable cache
|
||||
cache_duration = 600
|
||||
|
||||
# Control HTTPS certificate verification. Set it to false if you're using a self-signed certificate
|
||||
verify_cert = true
|
||||
|
|
|
@ -21,6 +21,7 @@ packages = find:
|
|||
install_requires =
|
||||
mopidy
|
||||
requests
|
||||
requests_oauthlib
|
||||
pygobject
|
||||
vext
|
||||
|
||||
|
|
|
@ -8,13 +8,17 @@ FUNKWHALE_URL = "https://test.funkwhale"
|
|||
|
||||
|
||||
@pytest.fixture()
|
||||
def config():
|
||||
def config(tmpdir):
|
||||
return {
|
||||
"core": {"data_dir": str(tmpdir)},
|
||||
"funkwhale": {
|
||||
"url": FUNKWHALE_URL,
|
||||
"username": "user",
|
||||
"password": "passw0rd",
|
||||
"cache_duration": 600,
|
||||
"client_id": "",
|
||||
"client_secret": "",
|
||||
"verify_cert": "",
|
||||
},
|
||||
"proxy": {},
|
||||
}
|
||||
|
@ -28,7 +32,10 @@ def backend(config):
|
|||
@pytest.fixture()
|
||||
def session(backend):
|
||||
return mopidy_funkwhale.client.get_requests_session(
|
||||
FUNKWHALE_URL, {}, "test/something"
|
||||
FUNKWHALE_URL,
|
||||
{},
|
||||
"test/something",
|
||||
base_cls=mopidy_funkwhale.client.SessionWithUrlBase,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -11,8 +11,11 @@ def test_get_default_config():
|
|||
assert "[funkwhale]" in config
|
||||
assert "enabled = true" in config
|
||||
assert "url = https://demo.funkwhale.audio" in config
|
||||
assert "username = demo" in config
|
||||
assert "password = demo" in config
|
||||
assert "username =" in config
|
||||
assert "password =" in config
|
||||
assert "client_id =" in config
|
||||
assert "client_secret =" in config
|
||||
assert "verify_cert =" in config
|
||||
|
||||
|
||||
def test_get_config_schema():
|
||||
|
|
Loading…
Reference in New Issue