butt/src/shoutcast.cpp
audioprog 031ca812ad init
2025-08-13 22:05:17 +02:00

403 lines
12 KiB
C++

// shoutcast functions for butt
//
// Copyright 2007-2018 by Daniel Noethen.
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef WIN32
#define usleep(us) Sleep(us / 1000)
#else
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h> //defines IPPROTO_TCP on BSD
#include <netdb.h>
#endif
#include <errno.h>
#include "gettext.h"
#include "cfg.h"
#include "butt.h"
#include "timer.h"
#include "strfuncs.h"
#include "shoutcast.h"
#include "parseconfig.h"
#include "sockfuncs.h"
#include "flgui.h"
#include "fl_funcs.h"
#include "url.h"
#include "uri_encode.h"
int server_version = SC_VERSION_UNKNOWN;
void send_icy_header(char *key, char *val)
{
char *icy_line;
int len;
if ((val == NULL) || (strlen(val) == 0)) {
return;
}
len = strlen(key) + strlen(val) + strlen(":\r\n") + 1;
icy_line = (char *)malloc(len * sizeof(char) + 1);
snprintf(icy_line, len, "%s:%s\r\n", key, val);
sock_send(stream_socket, icy_line, strlen(icy_line), SEND_TIMEOUT);
free(icy_line);
}
int sc_connect(void)
{
int ret;
char recv_buf[100];
char send_buf[100];
static bool error_printed = 0;
server_version = SC_VERSION_UNKNOWN;
stream_socket = sock_connect(cfg.srv[cfg.selected_srv]->addr, cfg.srv[cfg.selected_srv]->port + 1, SOCK_PROTO_TCP, CONN_TIMEOUT);
if (stream_socket < 0) {
switch (stream_socket) {
case SOCK_ERR_CREATE:
if (!error_printed) {
print_info(_("\nConnect: Could not create network socket"), 1);
}
if (cfg.main.force_reconnecting == 1) {
error_printed = 1;
ret = SC_RETRY;
}
else {
ret = SC_ABORT;
}
break;
case SOCK_ERR_RESOLVE:
if (!error_printed) {
print_info(_("\nConnect: Error resolving server address"), 1);
error_printed = 1;
}
ret = SC_RETRY;
break;
case SOCK_TIMEOUT:
case SOCK_INVALID:
ret = SC_RETRY;
break;
default:
if (cfg.main.force_reconnecting == 1) {
ret = SC_RETRY;
}
else {
ret = SC_ABORT;
}
}
return ret;
}
snprintf(send_buf, sizeof(send_buf), "%s%s", cfg.srv[cfg.selected_srv]->pwd, "\r\n");
sock_send(stream_socket, send_buf, strlen(send_buf), SEND_TIMEOUT);
// Make butt compatible to proxies/load balancers. Thanks to boyska
if (cfg.srv[cfg.selected_srv]->port == 80) {
snprintf(send_buf, sizeof(send_buf), "Host: %s\r\n", cfg.srv[cfg.selected_srv]->addr);
}
else {
snprintf(send_buf, sizeof(send_buf), "Host: %s:%d\r\n", cfg.srv[cfg.selected_srv]->addr, cfg.srv[cfg.selected_srv]->port);
}
sock_send(stream_socket, send_buf, strlen(send_buf), SEND_TIMEOUT);
if (cfg.main.num_of_icy > 0) {
char *icy_name = strdup(cfg.icy[cfg.selected_icy]->name);
if (cfg.icy[cfg.selected_icy]->expand_variables == 1) {
expand_string(&icy_name);
strrpl(&icy_name, (char *)"%N", cfg.srv[cfg.selected_srv]->name, MODE_ALL);
}
snprintf(send_buf + strlen(send_buf), sizeof(send_buf) - strlen(send_buf), "ice-name: %s\r\n", icy_name);
send_icy_header((char *)"icy-name", icy_name);
send_icy_header((char *)"icy-genre", cfg.icy[cfg.selected_icy]->genre);
send_icy_header((char *)"icy-url", cfg.icy[cfg.selected_icy]->url);
send_icy_header((char *)"icy-irc", cfg.icy[cfg.selected_icy]->irc);
send_icy_header((char *)"icy-icq", cfg.icy[cfg.selected_icy]->icq);
send_icy_header((char *)"icy-aim", cfg.icy[cfg.selected_icy]->aim);
send_icy_header((char *)"icy-pub", cfg.icy[cfg.selected_icy]->pub);
free(icy_name);
}
else {
send_icy_header((char *)"icy-name", (char *)"No Name");
send_icy_header((char *)"icy-pub", (char *)"0");
}
snprintf(send_buf, sizeof(send_buf), "%u", cfg.audio.bitrate);
send_icy_header((char *)"icy-br", send_buf);
sock_send(stream_socket, "content-type:", 13, SEND_TIMEOUT);
if (!strcmp(cfg.audio.codec, "mp3")) {
strcpy(send_buf, "audio/mpeg");
}
else if (!strcmp(cfg.audio.codec, "aac")) {
strcpy(send_buf, "audio/aac");
}
else {
strcpy(send_buf, "audio/ogg");
}
sock_send(stream_socket, send_buf, strlen(send_buf), SEND_TIMEOUT);
sock_send(stream_socket, "\r\n\r\n", 4, SEND_TIMEOUT);
if ((ret = sock_recv(stream_socket, recv_buf, sizeof(recv_buf) - 1, 5 * RECV_TIMEOUT)) == 0) {
usleep(100 * 1000);
sc_disconnect();
return SC_RETRY;
}
if (ret == SOCK_TIMEOUT) {
print_info(_("\nconnect: connection timed out. Trying again...\n"), 1);
usleep(100 * 1000);
sc_disconnect();
return SC_RETRY;
}
if (ret < 0) {
usleep(100 * 1000);
sc_disconnect();
return SC_RETRY;
}
recv_buf[ret] = '\0';
if ((recv_buf[0] != 'O') || (recv_buf[1] != 'K') || (ret <= 2)) {
if (strstr(strtolower(recv_buf), "invalid password") != NULL) {
if (!error_printed) {
print_info(_("\nConnect: Invalid password!\n"), 1);
}
sc_disconnect();
if (cfg.main.force_reconnecting == 1) {
error_printed = 1;
return SC_RETRY;
}
else {
return SC_ABORT;
}
}
sc_disconnect();
return SC_RETRY;
}
connected = 1;
error_printed = 0;
timer_init(&stream_timer, 1); // starts the "online" timer
timer_start(&stream_timer);
return SC_OK;
}
int sc_send(char *buf, int buf_len)
{
int ret;
ret = sock_send(stream_socket, buf, buf_len, SEND_TIMEOUT);
if (ret == SOCK_TIMEOUT) {
ret = -1;
}
return ret;
}
int sc_update_song(char *song_name)
{
int ret;
int web_socket;
char send_buf[1024];
char *song_buf;
web_socket = sock_connect(cfg.srv[cfg.selected_srv]->addr, cfg.srv[cfg.selected_srv]->port, SOCK_PROTO_TCP, CONN_TIMEOUT);
if (web_socket < 0) {
switch (web_socket) {
case SOCK_ERR_CREATE:
print_info(_("\nUpdate song: Could not create network socket"), 1);
ret = SC_ABORT;
break;
case SOCK_ERR_RESOLVE:
print_info(_("\nUpdate song: Error resolving server address"), 1);
ret = SC_ABORT;
break;
case SOCK_TIMEOUT:
case SOCK_INVALID:
ret = SC_RETRY;
break;
default:
ret = SC_ABORT;
}
return ret;
}
song_buf = (char *)malloc(strlen(song_name) * 3 + 1);
uri_encode(song_name, strlen(song_name), song_buf);
snprintf(send_buf, sizeof(send_buf),
"GET /admin.cgi?pass=%s&mode=updinfo&song=%s&url= HTTP/1.0\r\n"
"User-Agent: ShoutcastDSP (Mozilla Compatible)\r\n",
cfg.srv[cfg.selected_srv]->pwd, song_buf);
sock_send(web_socket, send_buf, strlen(send_buf), SEND_TIMEOUT);
if (cfg.srv[cfg.selected_srv]->port == 80) {
snprintf(send_buf, sizeof(send_buf), "Host: %s\r\n\r\n", cfg.srv[cfg.selected_srv]->addr);
}
else {
snprintf(send_buf, sizeof(send_buf), "Host: %s:%d\r\n\r\n", cfg.srv[cfg.selected_srv]->addr, cfg.srv[cfg.selected_srv]->port);
}
sock_send(web_socket, send_buf, strlen(send_buf), SEND_TIMEOUT);
sock_close(web_socket);
free(song_buf);
return SC_OK;
}
void sc_disconnect(void)
{
sock_close(stream_socket);
}
int sc_parse_sc1_response(char *response)
{
char *listeners;
char *listeners_start;
char *listeners_end;
// <HTML><meta http-equiv="Pragma" content="no-cache"></head><body>0,1,1,25,0,128,</body></html>
// Numbers in comma list mean:
// current_listeners,server_online,peak_listers,max_allowed_isteners,uniq,bitrate,songname
listeners_start = strstr(strtolower(response), "<body>");
if (listeners_start != NULL) {
listeners_end = strchr(listeners_start, ',');
if (listeners_end != NULL) {
listeners = listeners_start + strlen("<body>");
*listeners_end = '\0';
return atoi(listeners);
}
}
return -1;
}
int sc_parse_sc2_response(char *response)
{
char *listeners;
char *listeners_start;
char *listeners_end;
listeners_start = strstr(strtoupper(response), "<CURRENTLISTENERS>");
listeners_end = strstr(strtoupper(response), "</CURRENTLISTENERS>");
if (listeners_start != NULL && listeners_end != NULL) {
listeners = listeners_start + strlen("<CURRENTLISTENERS>");
*listeners_end = '\0';
return atoi(listeners);
}
return -1;
}
int sc_get_listener_count_from_url(char *url)
{
uint32_t data_size;
char recv_buf[100 * 1024];
if (strstr(cfg.srv[cfg.selected_srv]->custom_listener_url, "/stats?sid") != NULL) {
data_size = url_get_listener_count(url, recv_buf, sizeof(recv_buf));
if (data_size > 0) {
return sc_parse_sc2_response(recv_buf);
}
}
else if (strstr(cfg.srv[cfg.selected_srv]->custom_listener_url, "/7.html") != NULL) {
data_size = url_get_listener_count(url, recv_buf, sizeof(recv_buf));
if (data_size > 0) {
return sc_parse_sc1_response(recv_buf);
}
}
return -1;
}
int sc_get_listener_count(void)
{
uint32_t data_size;
char *sid;
char url[256];
char proto[8];
char recv_buf[100 * 1024];
/*
if (cfg.srv[cfg.selected_srv]->tls == 1) {
strncpy(proto, "https", sizeof(proto));
}
else {
strncpy(proto, "http", sizeof(proto));
}
*/
strncpy(proto, "http", sizeof(proto));
if (server_version == SC_VERSION_2 || server_version == SC_VERSION_UNKNOWN) {
sid = strstr(cfg.srv[cfg.selected_srv]->pwd, ":#");
if (sid != NULL) {
sid += 2;
snprintf(url, sizeof(url), "%s://%s:%d/stats?sid=%s", proto, cfg.srv[cfg.selected_srv]->addr, cfg.srv[cfg.selected_srv]->port, sid);
}
else {
snprintf(url, sizeof(url), "%s://%s:%d/stats?sid=1", proto, cfg.srv[cfg.selected_srv]->addr, cfg.srv[cfg.selected_srv]->port);
}
data_size = url_get_listener_count(url, recv_buf, sizeof(recv_buf));
if (data_size > 0) {
int listener = sc_parse_sc2_response(recv_buf);
if (listener != -1) {
server_version = SC_VERSION_2;
}
return listener;
}
}
if (server_version == SC_VERSION_1 || server_version == SC_VERSION_UNKNOWN) {
if (cfg.srv[cfg.selected_srv]->custom_listener_url == NULL || strlen(cfg.srv[cfg.selected_srv]->custom_listener_url) == 0) {
snprintf(url, sizeof(url), "%s://%s:%d/7.html", proto, cfg.srv[cfg.selected_srv]->addr, cfg.srv[cfg.selected_srv]->port);
}
else {
snprintf(url, sizeof(url), "%s", cfg.srv[cfg.selected_srv]->custom_listener_url);
}
data_size = url_get_listener_count(url, recv_buf, sizeof(recv_buf));
if (data_size > 0) {
int listener = sc_parse_sc1_response(recv_buf);
if (listener != -1) {
server_version = SC_VERSION_1;
}
return listener;
}
}
return -1;
}