1<?php
2/*
3 * vim:set softtabstop=4 shiftwidth=4 expandtab:
4 *
5 * LICENSE: GNU Affero General Public License, version 3 (AGPL-3.0-or-later)
6 * Copyright 2001 - 2020 Ampache.org
7 *
8 * This program is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU Affero General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 * GNU Affero General Public License for more details.
17 *
18 * You should have received a copy of the GNU Affero General Public License
19 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
20 *
21 */
22
23declare(strict_types=0);
24
25namespace Ampache\Module\Api;
26
27use Ampache\Module\Authentication\AuthenticationManagerInterface;
28use Ampache\Config\AmpConfig;
29use Ampache\Module\Authorization\AccessLevelEnum;
30use Ampache\Module\Authorization\Check\NetworkCheckerInterface;
31use Ampache\Module\System\Core;
32use Ampache\Repository\Model\Preference;
33use Ampache\Repository\Model\User;
34
35final class SubsonicApiApplication implements ApiApplicationInterface
36{
37    private AuthenticationManagerInterface $authenticationManager;
38
39    private NetworkCheckerInterface $networkChecker;
40
41    public function __construct(
42        AuthenticationManagerInterface $authenticationManager,
43        NetworkCheckerInterface $networkChecker
44    ) {
45        $this->authenticationManager = $authenticationManager;
46        $this->networkChecker        = $networkChecker;
47    }
48
49    public function run(): void
50    {
51        if (!AmpConfig::get('subsonic_backend')) {
52            echo T_("Disabled");
53
54            return;
55        }
56
57        $action = strtolower($_REQUEST['ssaction']);
58        // Compatibility reason
59        if (empty($action)) {
60            $action = strtolower(Core::get_request('action'));
61        }
62        $f        = ($_REQUEST['f']) ?: 'xml';
63        $callback = $_REQUEST['callback'];
64        /* Set the correct default headers */
65        if ($action != "getcoverart" && $action != "hls" && $action != "stream" && $action != "download" && $action != "getavatar") {
66            Subsonic_Api::setHeader($f);
67        }
68
69        // If we don't even have access control on then we can't use this!
70        if (!AmpConfig::get('access_control')) {
71            debug_event('rest/index', 'Error Attempted to use Subsonic API with Access Control turned off', 3);
72            ob_end_clean();
73            Subsonic_Api::apiOutput2($f, Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_UNAUTHORIZED, T_('Access Control not Enabled')), $callback);
74
75            return;
76        }
77
78        // Authenticate the user with preemptive HTTP Basic authentication first
79        $userName = Core::get_server('PHP_AUTH_USER');
80
81        if (empty($userName)) {
82            $userName = $_REQUEST['u'];
83        }
84        $password = Core::get_server('PHP_AUTH_PW');
85        if (empty($password)) {
86            $password = (string) $_REQUEST['p'];
87            $token    = (string) $_REQUEST['t'];
88            $salt     = (string) $_REQUEST['s'];
89        } else {
90            $token = '';
91            $salt  = '';
92        }
93        $version   = $_REQUEST['v'];
94        $clientapp = $_REQUEST['c'];
95
96        if (!filter_has_var(INPUT_SERVER, 'HTTP_USER_AGENT')) {
97            $_SERVER['HTTP_USER_AGENT'] = $clientapp;
98        }
99
100        if (empty($userName) || (empty($password) && (empty($token) || empty($salt))) || empty($version) || empty($action) || empty($clientapp)) {
101            ob_end_clean();
102            debug_event('rest/index', 'Missing Subsonic base parameters', 3);
103            Subsonic_Api::apiOutput2($f, Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_MISSINGPARAM, 'Missing Subsonic base parameters', $version), $callback);
104
105            return;
106        }
107
108        $password = Subsonic_Api::decrypt_password($password);
109
110        // Check user authentication
111        $auth = $this->authenticationManager->tokenLogin($userName, $token, $salt);
112        if ($auth === []) {
113            $auth = $this->authenticationManager->login($userName, $password, true);
114        }
115        if (!$auth['success']) {
116            debug_event('rest/index', 'Invalid authentication attempt to Subsonic API for user [' . $userName . ']', 3);
117            ob_end_clean();
118            Subsonic_Api::apiOutput2($f, Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_BADAUTH, 'Invalid authentication attempt to Subsonic API for user [' . $userName . ']', $version), $callback);
119
120            return;
121        }
122
123        $user            = User::get_from_username($userName);
124        $GLOBALS['user'] = $user;
125
126        if (!$this->networkChecker->check(AccessLevelEnum::TYPE_API, $user->id, AccessLevelEnum::LEVEL_GUEST)) {
127            debug_event('rest/index', 'Unauthorized access attempt to Subsonic API [' . Core::get_server('REMOTE_ADDR') . ']', 3);
128            ob_end_clean();
129            Subsonic_Api::apiOutput2($f, Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_UNAUTHORIZED, 'Unauthorized access attempt to Subsonic API - ACL Error', $version), $callback);
130
131            return;
132        }
133
134        // Check server version
135        if (
136            version_compare(Subsonic_Xml_Data::API_VERSION, $version) < 0 &&
137            !($clientapp == 'Sublime Music' && $version == '1.15.0')
138        ) {
139            ob_end_clean();
140            debug_event('rest/index', 'Requested client version is not supported', 3);
141            Subsonic_Api::apiOutput2($f, Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_APIVERSION_SERVER, 'Requested client version is not supported', $version), $callback);
142
143            return;
144        }
145        Preference::init();
146
147        // Get the list of possible methods for the Ampache API
148        $methods = get_class_methods(Subsonic_Api::class);
149
150        // Define list of internal functions that should be skipped
151        $internal_functions = array('check_version', 'check_parameter', 'decrypt_password', 'follow_stream', '_updatePlaylist', '_setStar', 'setHeader', 'apiOutput', 'apiOutput2', 'xml2json');
152
153        // We do not use $_GET because of multiple parameters with the same name
154        $query_string = Core::get_server('QUERY_STRING');
155        // Trick to avoid $HTTP_RAW_POST_DATA
156        $postdata = file_get_contents("php://input");
157        if (!empty($postdata)) {
158            $query_string .= '&' . $postdata;
159        }
160        $query  = explode('&', $query_string);
161        $params = array();
162        foreach ($query as $param) {
163            list($name, $value) = explode('=', $param);
164            $decname            = urldecode($name);
165            $decvalue           = urldecode($value);
166
167            // workaround for clementine/Qt5 bug
168            // see https://github.com/clementine-player/Clementine/issues/6080
169            $matches = array();
170            if ($decname == "id" && preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $decvalue, $matches)) {
171                $calc = (($matches[1] << 24) + ($matches[2] << 16) + ($matches[3] << 8) + $matches[4]);
172                if ($calc) {
173                    debug_event('rest/index', "Got id parameter $decvalue, which looks like an IP address. This is a known bug in some players, rewriting it to $calc", 4);
174                    $decvalue = $calc;
175                } else {
176                    debug_event('rest/index', "Got id parameter $decvalue, which looks like an IP address. Recalculation of the correct id failed, though", 3);
177                }
178            }
179
180            if (array_key_exists($decname, $params)) {
181                if (!is_array($params[$decname])) {
182                    $oldvalue           = $params[$decname];
183                    $params[$decname]   = array();
184                    $params[$decname][] = $oldvalue;
185                }
186                $params[$decname][] = $decvalue;
187            } else {
188                $params[$decname] = $decvalue;
189            }
190        }
191        //debug_event('rest/index', print_r($params, true), 5);
192        //debug_event('rest/index', print_r(apache_request_headers(), true), 5);
193
194        // Recurse through them and see if we're calling one of them
195        foreach ($methods as $method) {
196            if (in_array($method, $internal_functions)) {
197                continue;
198            }
199
200            // If the method is the same as the action being called
201            // Then let's call this function!
202
203            if ($action == $method) {
204                call_user_func(array(Subsonic_Api::class, $method), $params);
205                // We only allow a single function to be called, and we assume it's cleaned up!
206                return;
207            }
208        } // end foreach methods in API
209
210        // If we manage to get here, we still need to hand out an XML document
211        ob_end_clean();
212        Subsonic_Api::apiOutput2($f, Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_DATA_NOTFOUND,
213            'Subsonic_Api', $version), $callback);
214    }
215}
216