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