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