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\Channel; 26 27use Ahc\Cli\IO\Interactor; 28use Ampache\Config\ConfigContainerInterface; 29use Ampache\Repository\Model\Channel; 30use Ampache\Repository\Model\Song; 31use RuntimeException; 32 33final class HttpServer implements HttpServerInterface 34{ 35 private ConfigContainerInterface $configContainer; 36 37 public function __construct( 38 ConfigContainerInterface $configContainer 39 ) { 40 $this->configContainer = $configContainer; 41 } 42 43 public function serve( 44 Interactor $interactor, 45 Channel $channel, 46 array &$client_socks, 47 array &$stream_clients, 48 array &$read_socks, 49 $sock 50 ): void { 51 $data = fread($sock, 1024); 52 if (!$data) { 53 $this->disconnect($interactor, $channel, $client_socks, $stream_clients, $sock); 54 55 return; 56 } 57 58 $headers = explode("\n", $data); 59 $h_count = count($headers); 60 61 if ($h_count > 0) { 62 $cmd = explode(" ", $headers[0]); 63 if ($cmd['0'] == 'GET') { 64 switch ($cmd['1']) { 65 case '/stream.' . $channel->stream_type: 66 $options = array( 67 'socket' => $sock, 68 'length' => 0, 69 'isnew' => 1 70 ); 71 72 //debug_event('channel_run', 'HTTP HEADERS: '.$data, 5); 73 for ($count = 1; $count < $h_count; $count++) { 74 $headerpart = explode(":", $headers[$count], 2); 75 $header = strtolower(trim($headerpart[0])); 76 $value = trim($headerpart[1]); 77 switch ($header) { 78 case 'icy-metadata': 79 $options['metadata'] = ($value == '1'); 80 $options['metadata_lastsent'] = 0; 81 $options['metadata_lastsong'] = 0; 82 break; 83 } 84 } 85 86 // Stream request 87 if ($options['metadata']) { 88 //$http = "ICY 200 OK\r\n"); 89 $http = "HTTP/1.0 200 OK\r\n"; 90 } else { 91 $http = "HTTP/1.1 200 OK\r\n"; 92 $http .= "Cache-Control: no-store, no-cache, must-revalidate\r\n"; 93 } 94 $http .= "Content-Type: " . Song::type_to_mime($channel->stream_type) . "\r\n"; 95 $http .= "Accept-Ranges: none\r\n"; 96 97 $genre = $channel->get_genre(); 98 // Send Shoutcast metadata on demand 99 //if ($options['metadata']) { 100 $http .= "icy-notice1: " . $this->configContainer->get('site_title') . "\r\n"; 101 $http .= "icy-name: " . $channel->name . "\r\n"; 102 if (!empty($genre)) { 103 $http .= "icy-genre: " . $genre . "\r\n"; 104 } 105 $http .= "icy-url: " . $channel->url . "\r\n"; 106 $http .= "icy-pub: " . (($channel->is_private) ? "0" : "1") . "\r\n"; 107 if ($channel->bitrate) { 108 $http .= "icy-br: " . strval($channel->bitrate) . "\r\n"; 109 } 110 global $metadata_interval; 111 $http .= "icy-metaint: " . strval($metadata_interval) . "\r\n"; 112 //} 113 // Send additional Icecast metadata 114 $http .= "x-audiocast-server-url: " . $channel->url . "\r\n"; 115 $http .= "x-audiocast-name: " . $channel->name . "\r\n"; 116 $http .= "x-audiocast-description: " . $channel->description . "\r\n"; 117 $http .= "x-audiocast-url: " . $channel->url . "\r\n"; 118 if (!empty($genre)) { 119 $http .= "x-audiocast-genre: " . $genre . "\r\n"; 120 } 121 $http .= "x-audiocast-bitrate: " . strval($channel->bitrate) . "\r\n"; 122 $http .= "x-audiocast-public: " . (($channel->is_private) ? "0" : "1") . "\r\n"; 123 124 $http .= "\r\n"; 125 126 fwrite($sock, $http); 127 128 // Add to stream clients list 129 $key = array_search($sock, $read_socks); 130 $stream_clients[$key] = $options; 131 break; 132 case '/': 133 case '/status.xsl': 134 // Stream request 135 fwrite($sock, "HTTP/1.0 200 OK\r\n"); 136 fwrite($sock, "Cache-Control: no-store, no-cache, must-revalidate\r\n"); 137 fwrite($sock, "Content-Type: text/html\r\n"); 138 fwrite($sock, "\r\n"); 139 140 // Create xsl structure 141 // Header 142 $xsl = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" . "\n"; 143 $xsl .= "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">" . "\n"; 144 $xsl .= "<html xmlns=\"http://www.w3.org/1999/xhtml\">" . "\n"; 145 $xsl .= "<head>" . "\n"; 146 $xsl .= "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />" . "\n"; 147 $xsl .= "<title>" . T_("Icecast Streaming Media Server") . " - " . T_('Ampache') . "</title>" . "\n"; 148 $xsl .= "<link rel=\"stylesheet\" type=\"text/css\" href=\"style.css\" />" . "\n"; 149 $xsl .= "<link rel=\"shortcut icon\" href=\"favicon.ico\" />"; 150 $xsl .= "</head>" . "\n"; 151 $xsl .= "<body>" . "\n"; 152 $xsl .= "<div class=\"main\">" . "\n"; 153 154 // Content 155 $xsl .= "<div class=\"roundcont\">" . "\n"; 156 $xsl .= "<div class=\"roundtop\">" . "\n"; 157 $xsl .= "<img src=\"images/corner_topleft.jpg\" class=\"corner\" style=\"display: none\" alt=\"\" />" . "\n"; 158 $xsl .= "</div>" . "\n"; 159 $xsl .= "<div class=\"newscontent\">" . "\n"; 160 $xsl .= "<div class=\"streamheader\">" . "\n"; 161 $xsl .= "<table>" . "\n"; 162 $xsl .= "<colgroup align=\"left\"></colgroup>" . "\n"; 163 $xsl .= "<colgroup align=\"right\" width=\"300\"></colgroup>" . "\n"; 164 $xsl .= "<tr>" . "\n"; 165 $xsl .= "<td><h3>Mount Point: <a href=\"stream." . $channel->stream_type . "\">stream." . $channel->stream_type . "</a></h3></td>" . "\n"; 166 $xsl .= "<td align=\"right\">" . "\n"; 167 $xsl .= "<a href=\"stream." . $channel->stream_type . ".m3u\">M3U</a>" . "\n"; 168 $xsl .= "</td>" . "\n"; 169 $xsl .= "</tr>" . "\n"; 170 $xsl .= "</table>" . "\n"; 171 $xsl .= "</div>" . "\n"; 172 $xsl .= "<table>" . "\n"; 173 $xsl .= "<tr>" . "\n"; 174 $xsl .= "<td>Stream Title:</td>" . "\n"; 175 $xsl .= "<td class=\"streamdata\">" . $channel->name . "</td>" . "\n"; 176 $xsl .= "</tr>" . "\n"; 177 $xsl .= "<tr>" . "\n"; 178 $xsl .= "<td>Stream Description:</td>" . "\n"; 179 $xsl .= "<td class=\"streamdata\">" . $channel->description . "</td>" . "\n"; 180 $xsl .= "</tr>" . "\n"; 181 $xsl .= "<tr>" . "\n"; 182 $xsl .= "<td>Content Type:</td>" . "\n"; 183 $xsl .= "<td class=\"streamdata\">" . Song::type_to_mime($channel->stream_type) . "</td>" . "\n"; 184 $xsl .= "</tr>" . "\n"; 185 $xsl .= "<tr>" . "\n"; 186 $xsl .= "<td>Mount Start:</td>" . "\n"; 187 $xsl .= "<td class=\"streamdata\">" . get_datetime($channel->start_date) . "</td>" . "\n"; 188 $xsl .= "</tr>" . "\n"; 189 $xsl .= "<tr>" . "\n"; 190 $xsl .= "<td>Bitrate:</td>" . "\n"; 191 $xsl .= "<td class=\"streamdata\">" . $channel->bitrate . "</td>" . "\n"; 192 $xsl .= "</tr>" . "\n"; 193 $xsl .= "<tr>" . "\n"; 194 $xsl .= "<td>Current Listeners:</td>" . "\n"; 195 $xsl .= "<td class=\"streamdata\">" . $channel->listeners . "</td>" . "\n"; 196 $xsl .= "</tr>" . "\n"; 197 $xsl .= "<tr>" . "\n"; 198 $xsl .= "<td>Peak Listeners:</td>" . "\n"; 199 $xsl .= "<td class=\"streamdata\">" . $channel->peak_listeners . "</td>" . "\n"; 200 $xsl .= "</tr>" . "\n"; 201 $genre = $channel->get_genre(); 202 $xsl .= "<tr>" . "\n"; 203 $xsl .= "<td>Stream Genre:</td>" . "\n"; 204 $xsl .= "<td class=\"streamdata\">" . $genre . "</td>" . "\n"; 205 $xsl .= "</tr>" . "\n"; 206 $xsl .= "<tr>" . "\n"; 207 $xsl .= "<td>Stream URL:</td>" . "\n"; 208 $xsl .= "<td class=\"streamdata\"><a href=\"" . $channel->url . "\" target=\"_blank\">" . $channel->url . "</a></td>" . "\n"; 209 $xsl .= "</tr>" . "\n"; 210 $currentsong = ""; 211 if ($channel->media) { 212 $currentsong = $channel->media->f_artist . " - " . $channel->media->f_title; 213 } 214 $xsl .= "<tr>" . "\n"; 215 $xsl .= "<td>Current Song:</td>" . "\n"; 216 $xsl .= "<td class=\"streamdata\">" . $currentsong . "</td>" . "\n"; 217 $xsl .= "</tr>" . "\n"; 218 $xsl .= "</table>" . "\n"; 219 $xsl .= "</div>" . "\n"; 220 $xsl .= "<div class=\"roundbottom\">" . "\n"; 221 $xsl .= "<img src=\"images/corner_bottomleft.jpg\" class=\"corner\" style=\"display: none\" alt=\"\" />" . "\n"; 222 $xsl .= "</div>" . "\n"; 223 $xsl .= "</div>" . "\n"; 224 $xsl .= "<br /><br />" . "\n"; 225 226 // Footer 227 $xsl .= "<div class=\"poster\">" . "\n"; 228 $xsl .= "Support Ampache at <a target=\"_blank\" href=\"http://www.ampache.org\">www.ampache.org</a>" . "\n"; 229 $xsl .= "</div>" . "\n"; 230 $xsl .= "</div>" . "\n"; 231 $xsl .= "</body>" . "\n"; 232 $xsl .= "</html>" . "\n"; 233 234 fwrite($sock, $xsl); 235 236 fclose($sock); 237 unset($client_socks[array_search($sock, $client_socks)]); 238 break; 239 case '/style.css': 240 case '/favicon.ico': 241 case '/images/corner_bottomleft.jpg': 242 case '/images/corner_bottomright.jpg': 243 case '/images/corner_topleft.jpg': 244 case '/images/corner_topright.jpg': 245 case '/images/icecast.png': 246 case '/images/key.png': 247 case '/images/tunein.png': 248 // Get read file data 249 $fpath = __DIR__ . '/../../../public/channel' . $cmd['1']; 250 $pinfo = pathinfo($fpath); 251 252 $content_type = 'text/html'; 253 switch ($pinfo['extension']) { 254 case 'css': 255 $content_type = "text/css"; 256 break; 257 case 'jpg': 258 $content_type = "image/jpeg"; 259 break; 260 case 'png': 261 $content_type = "image/png"; 262 break; 263 case 'ico': 264 $content_type = "image/vnd.microsoft.icon"; 265 break; 266 } 267 fwrite($sock, "HTTP/1.0 200 OK\r\n"); 268 fwrite($sock, "Content-Type: " . $content_type . "\r\n"); 269 $fdata = file_get_contents($fpath); 270 fwrite($sock, "Content-Length: " . strlen($fdata) . "\r\n"); 271 fwrite($sock, "\r\n"); 272 fwrite($sock, $fdata); 273 fclose($sock); 274 unset($client_socks[array_search($sock, $client_socks)]); 275 break; 276 case '/stream.' . $channel->stream_type . '.m3u': 277 fwrite($sock, "HTTP/1.0 200 OK\r\n"); 278 fwrite($sock, "Cache-control: public\r\n"); 279 fwrite($sock, "Content-Disposition: filename=stream." . $channel->stream_type . ".m3u\r\n"); 280 fwrite($sock, "Content-Type: audio/x-mpegurl\r\n"); 281 fwrite($sock, "\r\n"); 282 283 fwrite($sock, $channel->get_stream_url() . "\n"); 284 285 fclose($sock); 286 unset($client_socks[array_search($sock, $client_socks)]); 287 break; 288 default: 289 debug_event('channel_run', 'Unknown request. Closing connection.', 3); 290 fclose($sock); 291 unset($client_socks[array_search($sock, $client_socks)]); 292 break; 293 } 294 } 295 } 296 } 297 298 public function disconnect( 299 Interactor $interactor, 300 Channel $channel, 301 array &$client_socks, 302 array &$stream_clients, 303 $sock 304 ): void { 305 $key = array_search($sock, $client_socks); 306 unset($client_socks[$key]); 307 unset($stream_clients[$key]); 308 if (fclose($sock) === false) { 309 throw new RuntimeException('The file handle ' . $sock . ' could not be closed'); 310 } 311 $channel->update_listeners(count($client_socks)); 312 debug_event('channel_run', 'A client disconnected. Now there are total ' . count($client_socks) . ' clients.', 4); 313 314 $interactor->info('Client disconnected', true); 315 316 ob_flush(); 317 } 318} 319