Initial commit

master
Icedream 2014-05-01 07:31:05 +02:00
commit f47b51127c
19 changed files with 1356 additions and 0 deletions

184
.gitignore vendored Normal file
View File

@ -0,0 +1,184 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.sln.docstates
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
x64/
build/
bld/
[Bb]in/
[Oo]bj/
# Roslyn cache directories
*.ide/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
#NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opensdf
*.sdf
*.cachefile
# Visual Studio profiler
*.psess
*.vsp
*.vspx
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding addin-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# NCrunch
_NCrunch_*
.*crunch*.local.xml
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
## TODO: Comment the next line if you want to checkin your
## web deploy settings but do note that will include unencrypted
## passwords
*.pubxml
# NuGet Packages Directory
packages/*
## TODO: If the tool you use requires repositories.config
## uncomment the next line
#!packages/repositories.config
# Enable "build/" folder in the NuGet Packages folder since
# NuGet packages use it for MSBuild targets.
# This line needs to be after the ignore of the build folder
# (and the packages folder if the line above has been uncommented)
!packages/build/
# Windows Azure Build Output
csx/
*.build.csdef
# Windows Store app package directory
AppPackages/
# Others
sql/
*.Cache
ClientBin/
[Ss]tyle[Cc]op.*
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.pfx
*.publishsettings
node_modules/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/

6
.nuget/NuGet.Config Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<solution>
<add key="disableSourceControlIntegration" value="true" />
</solution>
</configuration>

BIN
.nuget/NuGet.exe Normal file

Binary file not shown.

144
.nuget/NuGet.targets Normal file
View File

@ -0,0 +1,144 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">$(MSBuildProjectDirectory)\..\</SolutionDir>
<!-- Enable the restore command to run before builds -->
<RestorePackages Condition=" '$(RestorePackages)' == '' ">false</RestorePackages>
<!-- Property that enables building a package from a project -->
<BuildPackage Condition=" '$(BuildPackage)' == '' ">false</BuildPackage>
<!-- Determines if package restore consent is required to restore packages -->
<RequireRestoreConsent Condition=" '$(RequireRestoreConsent)' != 'false' ">true</RequireRestoreConsent>
<!-- Download NuGet.exe if it does not already exist -->
<DownloadNuGetExe Condition=" '$(DownloadNuGetExe)' == '' ">false</DownloadNuGetExe>
</PropertyGroup>
<ItemGroup Condition=" '$(PackageSources)' == '' ">
<!-- Package sources used to restore packages. By default, registered sources under %APPDATA%\NuGet\NuGet.Config will be used -->
<!-- The official NuGet package source (https://www.nuget.org/api/v2/) will be excluded if package sources are specified and it does not appear in the list -->
<!--
<PackageSource Include="https://www.nuget.org/api/v2/" />
<PackageSource Include="https://my-nuget-source/nuget/" />
-->
</ItemGroup>
<PropertyGroup Condition=" '$(OS)' == 'Windows_NT'">
<!-- Windows specific commands -->
<NuGetToolsPath>$([System.IO.Path]::Combine($(SolutionDir), ".nuget"))</NuGetToolsPath>
</PropertyGroup>
<PropertyGroup Condition=" '$(OS)' != 'Windows_NT'">
<!-- We need to launch nuget.exe with the mono command if we're not on windows -->
<NuGetToolsPath>$(SolutionDir).nuget</NuGetToolsPath>
</PropertyGroup>
<PropertyGroup>
<PackagesProjectConfig Condition=" '$(OS)' == 'Windows_NT'">$(MSBuildProjectDirectory)\packages.$(MSBuildProjectName.Replace(' ', '_')).config</PackagesProjectConfig>
<PackagesProjectConfig Condition=" '$(OS)' != 'Windows_NT'">$(MSBuildProjectDirectory)\packages.$(MSBuildProjectName).config</PackagesProjectConfig>
</PropertyGroup>
<PropertyGroup>
<PackagesConfig Condition="Exists('$(MSBuildProjectDirectory)\packages.config')">$(MSBuildProjectDirectory)\packages.config</PackagesConfig>
<PackagesConfig Condition="Exists('$(PackagesProjectConfig)')">$(PackagesProjectConfig)</PackagesConfig>
</PropertyGroup>
<PropertyGroup>
<!-- NuGet command -->
<NuGetExePath Condition=" '$(NuGetExePath)' == '' ">$(NuGetToolsPath)\NuGet.exe</NuGetExePath>
<PackageSources Condition=" $(PackageSources) == '' ">@(PackageSource)</PackageSources>
<NuGetCommand Condition=" '$(OS)' == 'Windows_NT'">"$(NuGetExePath)"</NuGetCommand>
<NuGetCommand Condition=" '$(OS)' != 'Windows_NT' ">mono --runtime=v4.0.30319 $(NuGetExePath)</NuGetCommand>
<PackageOutputDir Condition="$(PackageOutputDir) == ''">$(TargetDir.Trim('\\'))</PackageOutputDir>
<RequireConsentSwitch Condition=" $(RequireRestoreConsent) == 'true' ">-RequireConsent</RequireConsentSwitch>
<NonInteractiveSwitch Condition=" '$(VisualStudioVersion)' != '' AND '$(OS)' == 'Windows_NT' ">-NonInteractive</NonInteractiveSwitch>
<PaddedSolutionDir Condition=" '$(OS)' == 'Windows_NT'">"$(SolutionDir) "</PaddedSolutionDir>
<PaddedSolutionDir Condition=" '$(OS)' != 'Windows_NT' ">"$(SolutionDir)"</PaddedSolutionDir>
<!-- Commands -->
<RestoreCommand>$(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(NonInteractiveSwitch) $(RequireConsentSwitch) -solutionDir $(PaddedSolutionDir)</RestoreCommand>
<BuildCommand>$(NuGetCommand) pack "$(ProjectPath)" -Properties "Configuration=$(Configuration);Platform=$(Platform)" $(NonInteractiveSwitch) -OutputDirectory "$(PackageOutputDir)" -symbols</BuildCommand>
<!-- We need to ensure packages are restored prior to assembly resolve -->
<BuildDependsOn Condition="$(RestorePackages) == 'true'">
RestorePackages;
$(BuildDependsOn);
</BuildDependsOn>
<!-- Make the build depend on restore packages -->
<BuildDependsOn Condition="$(BuildPackage) == 'true'">
$(BuildDependsOn);
BuildPackage;
</BuildDependsOn>
</PropertyGroup>
<Target Name="CheckPrerequisites">
<!-- Raise an error if we're unable to locate nuget.exe -->
<Error Condition="'$(DownloadNuGetExe)' != 'true' AND !Exists('$(NuGetExePath)')" Text="Unable to locate '$(NuGetExePath)'" />
<!--
Take advantage of MsBuild's build dependency tracking to make sure that we only ever download nuget.exe once.
This effectively acts as a lock that makes sure that the download operation will only happen once and all
parallel builds will have to wait for it to complete.
-->
<MsBuild Targets="_DownloadNuGet" Projects="$(MSBuildThisFileFullPath)" Properties="Configuration=NOT_IMPORTANT;DownloadNuGetExe=$(DownloadNuGetExe)" />
</Target>
<Target Name="_DownloadNuGet">
<DownloadNuGet OutputFilename="$(NuGetExePath)" Condition=" '$(DownloadNuGetExe)' == 'true' AND !Exists('$(NuGetExePath)')" />
</Target>
<Target Name="RestorePackages" DependsOnTargets="CheckPrerequisites">
<Exec Command="$(RestoreCommand)"
Condition="'$(OS)' != 'Windows_NT' And Exists('$(PackagesConfig)')" />
<Exec Command="$(RestoreCommand)"
LogStandardErrorAsError="true"
Condition="'$(OS)' == 'Windows_NT' And Exists('$(PackagesConfig)')" />
</Target>
<Target Name="BuildPackage" DependsOnTargets="CheckPrerequisites">
<Exec Command="$(BuildCommand)"
Condition=" '$(OS)' != 'Windows_NT' " />
<Exec Command="$(BuildCommand)"
LogStandardErrorAsError="true"
Condition=" '$(OS)' == 'Windows_NT' " />
</Target>
<UsingTask TaskName="DownloadNuGet" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<ParameterGroup>
<OutputFilename ParameterType="System.String" Required="true" />
</ParameterGroup>
<Task>
<Reference Include="System.Core" />
<Using Namespace="System" />
<Using Namespace="System.IO" />
<Using Namespace="System.Net" />
<Using Namespace="Microsoft.Build.Framework" />
<Using Namespace="Microsoft.Build.Utilities" />
<Code Type="Fragment" Language="cs">
<![CDATA[
try {
OutputFilename = Path.GetFullPath(OutputFilename);
Log.LogMessage("Downloading latest version of NuGet.exe...");
WebClient webClient = new WebClient();
webClient.DownloadFile("https://www.nuget.org/nuget.exe", OutputFilename);
return true;
}
catch (Exception ex) {
Log.LogErrorFromException(ex);
return false;
}
]]>
</Code>
</Task>
</UsingTask>
</Project>

29
ShoutcastBridge.sln Normal file
View File

@ -0,0 +1,29 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2013
VisualStudioVersion = 12.0.21005.1
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ShoutcastBridge", "src\sc_bridge\ShoutcastBridge.csproj", "{491CCFC0-2C51-451F-92AE-056B0474AD7E}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{E4EA1463-A0D4-4D06-9F76-8FD55C5797CE}"
ProjectSection(SolutionItems) = preProject
.nuget\NuGet.Config = .nuget\NuGet.Config
.nuget\NuGet.exe = .nuget\NuGet.exe
.nuget\NuGet.targets = .nuget\NuGet.targets
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{491CCFC0-2C51-451F-92AE-056B0474AD7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{491CCFC0-2C51-451F-92AE-056B0474AD7E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{491CCFC0-2C51-451F-92AE-056B0474AD7E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{491CCFC0-2C51-451F-92AE-056B0474AD7E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,46 @@
using System;
using System.Net;
using System.Threading.Tasks;
namespace AFR.ShoutcastBridge
{
internal class AdminHandler : IHttpRequestHandler
{
private readonly ShoutcastBridgeServer _shoutcastBridge;
public AdminHandler(ShoutcastBridgeServer bridgeServer)
{
_shoutcastBridge = bridgeServer;
}
public Task Handle(IHttpContext context, Func<Task> next)
{
string mode;
context.Request.QueryString.TryGetByName("mode", out mode);
switch (mode.ToLower())
{
case "updinfo":
string password;
string song;
context.Request.QueryString.TryGetByName("pass", out password);
context.Request.QueryString.TryGetByName("song", out song);
if (string.IsNullOrEmpty(password))
break;
context.Response = HttpResponse.CreateWithMessage(HttpResponseCode.Ok, _shoutcastBridge.UpdateMetadata((IPEndPoint)context.RemoteEndPoint, password, song)
? "OK"
: "Could not update metadata",
context.Request.Headers.KeepAliveConnection());
break;
default:
context.Response = HttpResponse.CreateWithMessage(HttpResponseCode.Ok, "Unknown mode", context.Request.Headers.KeepAliveConnection());
break;
}
return Task.Factory.GetCompleted();
}
}
}

37
src/sc_bridge/Config.xml Normal file
View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<bridge>
<!--<ip>0.0.0.0</ip>-->
<port>61121</port>
<stream password="testing0">
<port>8000</port>
<mountpoint>/stream</mountpoint>
</stream>
<stream password="testing1">
<host>internal.listen.rekt.in</host>
<port>61120</port>
<username>source</username>
<password>testing</password>
<mountpoint>/afr-herp/live</mountpoint>
</stream>
<stream password="testing2">
<host>internal.listen.rekt.in</host>
<port>61120</port>
<username>source</username>
<password>testing</password>
<mountpoint>/afr-derp/live</mountpoint>
</stream>
<stream password="testing3">
<host>internal.listen.rekt.in</host>
<port>61120</port>
<username>source</username>
<password>testing</password>
<mountpoint>/afr-lurk/live</mountpoint>
</stream>
</bridge>

View File

@ -0,0 +1,176 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Web;
using log4net;
namespace AFR.ShoutcastBridge
{
public class IcecastWriter
{
private NetworkStream _ns;
private StreamReader _sr;
private StreamWriter _sw;
private TcpClient _tcp;
public IcecastWriter()
{
Hostname = "127.0.0.1";
Port = 8000;
Username = "source";
Password = "hackme";
ContentType = "audio/mpeg";
Description = "Livestream";
Name = "Livestream";
Genre = "Various";
Public = false;
}
public string Name { get; set; }
public string Genre { get; set; }
public string Url { get; set; }
public ushort KBitrate { get; set; }
public bool Public { get; set; }
public string Description { get; set; }
public string ContentType { get; set; }
public string Hostname { get; set; }
public ushort Port { get; set; }
public string Mountpoint { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public bool Connected
{
get { return _ns != null && _sr != null && _sw != null && _tcp != null && _tcp.Connected; }
}
public bool Open()
{
try
{
_tcp = new TcpClient(Hostname, Port);
_ns = _tcp.GetStream();
_sr = new StreamReader(_ns);
_sw = new StreamWriter(_ns) {AutoFlush = true};
// Request headers
_sw.WriteLine("SOURCE {0} ICE/1.0", Mountpoint);
_sw.WriteLine("content-type: {0}", ContentType);
_sw.WriteLine("Authorization: Basic {0}",
Convert.ToBase64String(Encoding.UTF8.GetBytes(string.Format("{0}:{1}", Username, Password))));
_sw.WriteLine("ice-name: {0}", Name);
_sw.WriteLine("ice-url: {0}", Url);
_sw.WriteLine("ice-genre: {0}", Genre);
_sw.WriteLine("ice-bitrate: {0}", KBitrate);
_sw.WriteLine("ice-private: {0}", Public ? 0 : 1);
_sw.WriteLine("ice-public: {0}", Public ? 1 : 0);
_sw.WriteLine("ice-description: {0}", Description);
_sw.WriteLine("ice-audio-info: ice-bitrate={0}", KBitrate);
_sw.WriteLine();
// Authorized?
string statusLine = _sr.ReadLine();
if (statusLine == null)
{
LogManager.GetLogger("IcecastWriter").ErrorFormat("Icecast socket error: No response");
return false;
}
string[] status = statusLine.Split(' ');
if (status[1] == "200")
// Now we can stream
return true;
// Something went wrong
LogManager.GetLogger("IcecastWriter").ErrorFormat("Icecast HTTP error: {0} {1}", status[1], status[2]);
Close();
return false;
}
catch (Exception error)
{
LogManager.GetLogger("IcecastWriter").ErrorFormat("BUG IN ICECASTWRITING: {0}", error);
// Something went wrong
Close();
return false;
}
}
public void Push(byte[] data)
{
try
{
if (_ns == null || _sr == null || _sw == null || _tcp == null || !_tcp.Connected)
return;
_sw.BaseStream.WriteAsync(data, 0, data.Length);
}
catch (Exception error)
{
LogManager.GetLogger("IcecastWriter").ErrorFormat("Icecast socket error while pushing data: {0}", error);
Close();
}
}
public bool SendMetadata(string song, bool tryOnce = false)
{
if (song == null)
song = string.Empty;
var reqQuery = new Dictionary<string, string>
{
//{"pass",Password},
{"mode", "updinfo"},
{"mount", Mountpoint},
{"song", song}
};
var reqUriBuilder = new UriBuilder("http", Hostname, Port, "/admin/metadata")
{
Query =
string.Join("&",
reqQuery.Keys.Select(
key =>
string.Format("{0}={1}", HttpUtility.UrlEncode(key),
HttpUtility.UrlEncode(reqQuery[key])))),
//UserName = Username,
//Password = Password
};
var req = (HttpWebRequest) WebRequest.Create(reqUriBuilder.Uri);
req.UserAgent = string.Format("ShoutcastBridge/{0}", Assembly.GetExecutingAssembly().GetName().Version);
req.Credentials = new NetworkCredential(Username, Password);
try
{
req.GetResponse();
return true;
}
catch (Exception error)
{
LogManager.GetLogger("IcecastWriter").WarnFormat("Uri: {0}", reqUriBuilder.Uri);
LogManager.GetLogger("IcecastWriter").WarnFormat("Could not send metadata: {0}", error.Message);
if (tryOnce)
return false;
Thread.Sleep(1000);
return SendMetadata(song, true);
}
}
public void Close()
{
LogManager.GetLogger("IcecastWriter").Info("Disconnecting");
if (_ns == null || _sr == null || _sw == null || _tcp == null || !_tcp.Connected)
return;
_sr.Dispose();
_sw.Dispose();
_ns.Dispose();
_tcp.Close();
}
}
}

192
src/sc_bridge/Program.cs Normal file
View File

@ -0,0 +1,192 @@
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Xml.Linq;
using log4net;
using log4net.Appender;
using log4net.Config;
using log4net.Core;
namespace AFR.ShoutcastBridge
{
class Program
{
private static ILog _log;
static int Main(string[] args)
{
var configFile = "Config.xml";
// Arguments
if (args.Any())
{
configFile = args[0];
}
// Initialize logging
BasicConfigurator.Configure(new ConsoleAppender()
{
Layout = new log4net.Layout.PatternLayout(@"[%logger] %level: %message%newline"),
Threshold = Level.Debug
});
_log = LogManager.GetLogger("bridgeapp");
// Configuration setup
XDocument xdoc;
if (!File.Exists(configFile))
{
_log.Error("Invalid configuration file given (file not found).");
return -2;
}
try
{
xdoc = XDocument.Load(configFile);
if (xdoc.Root == null)
{
_log.Error("Invalid configuration file given (could not parse file, no XML root found).");
return -2;
}
}
catch
{
_log.Error("Invalid configuration file given (could not parse file, any typos in the XML code?).");
return -2;
}
// Get ip address to work with
var ip = IPAddress.Any;
var ipElements = xdoc.Root.Elements("ip").ToArray();
if (ipElements.Any())
{
if (!IPAddress.TryParse(ipElements.First().Value, out ip))
{
_log.Error("Invalid IP address in configuration, please check the configuration file.");
return -1;
}
if (ipElements.Count() > 1)
_log.Warn("Multiple ip addresses defined, using first one. Please check the configuration file as soon as possible and fix this.");
}
// Get port to work with
var port = (ushort)8000;
var portElements = xdoc.Root.Elements("port").ToArray();
if (portElements.Any())
{
if (!ushort.TryParse(portElements.First().Value, out port))
{
_log.Error("Invalid port in configuration, please check the configuration file.");
return -1;
}
if (portElements.Count() > 1)
_log.Warn("Multiple ports defined, using first one. Please check the configuration file as soon as possible and fix this.");
}
var sb = new ShoutcastBridgeServer(ip, port);
// Mountpoint configuration
var mpElements = xdoc.Root.Descendants("stream").ToArray();
if (!mpElements.Any())
{
_log.Error("No mountpoints/streams configured. Please define at least one in the configuration file.");
return -1;
}
foreach (var mpElement in mpElements)
{
var pwAttr = mpElement.Attribute("password");
string pw;
if (pwAttr == null || string.IsNullOrEmpty(pw = pwAttr.Value))
{
_log.Error("Mountpoint without password defined. All mountpoints need a unique password.");
return -1;
}
if (mpElements.Count(m => m.Attribute("password").Value == pwAttr.Value) > 1)
{
_log.ErrorFormat("Too many mountpoints with same password ({0}) defined. All mountpoints need a unique password.", pwAttr.Value);
return -1;
}
ushort parsedPort;
var mpPortElements = mpElement.Elements("port").ToArray();
if (!mpPortElements.Any())
{
//mpElement.Add(new XElement("port", 8000));
//mpPortElements = mpElement.Elements("port").ToArray();
parsedPort = 8000;
}
else if (mpPortElements.Count() > 1)
{
_log.WarnFormat("Too many icecast ports defined for mountpoint pw={0}, please only define one.", pw);
return -1;
}
else if (!ushort.TryParse(mpPortElements.Single().Value, out parsedPort))
{
_log.ErrorFormat("Invalid icecast port defined for mountpoint pw={0}, please define a proper port number.", pw);
return -1;
}
var mpHostElements = mpElement.Elements("host").ToArray();
if (!mpHostElements.Any())
{
mpElement.Add(new XElement("host", IPAddress.Loopback.ToString()));
mpHostElements = mpElement.Elements("host").ToArray();
}
else if (mpHostElements.Count() > 1)
{
_log.WarnFormat("Too many icecast host names defined for mountpoint pw={0}, please only define one.", pw);
return -1;
}
var mpMountpointElements = mpElement.Elements("mountpoint").ToArray();
if (!mpMountpointElements.Any())
{
mpElement.Add(new XElement("mountpoint", "/stream"));
mpMountpointElements = mpElement.Elements("mountpoint").ToArray(); // yes, this is rude.
}
else if (mpMountpointElements.Count() > 1)
{
_log.WarnFormat(
"Too many icecast mountpoint names defined for mountpoint pw={0}, please only define one.", pw);
return -1;
}
var mpPasswordElements = mpElement.Elements("password").ToArray();
if (!mpPasswordElements.Any())
{
mpElement.Add(new XElement("password", "hackme"));
mpPasswordElements = mpElement.Elements("password").ToArray();
}
else if (mpPasswordElements.Count() > 1)
{
_log.WarnFormat("Too many icecast passwords defined for mountpoint pw={0}, please only define one.", pw);
return -1;
}
var mpUsernameElements = mpElement.Elements("username").ToArray();
if (!mpUsernameElements.Any())
{
mpElement.Add(new XElement("username", "source"));
mpUsernameElements = mpElement.Elements("username").ToArray();
}
else if (mpUsernameElements.Count() > 1)
{
_log.WarnFormat("Too many icecast usernames defined for mountpoint pw={0}, please only define one.", pw);
return -1;
}
sb.AddMountpoint(pw, new ShoutcastBridgeMountpoint()
{
IcecastHost = mpHostElements.Single().Value,
IcecastPort = parsedPort,
IcecastMountpoint = mpMountpointElements.Single().Value,
IcecastUsername = mpUsernameElements.Single().Value,
IcecastPassword = mpPasswordElements.Single().Value
});
}
_log.DebugFormat("Starting up Shoutcast bridge now.");
sb.Start();
return 0; // We'll never get here anyways... except on errors `-`
}
}
}

View File

@ -0,0 +1,35 @@
using System.Reflection;
using System.Runtime.InteropServices;
// Allgemeine Informationen über eine Assembly werden über die folgenden
// Attribute gesteuert. Ändern Sie diese Attributwerte, um die Informationen zu ändern,
// die mit einer Assembly verknüpft sind.
[assembly: AssemblyTitle("AFR.ShoutcastBridge")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Hewlett-Packard")]
[assembly: AssemblyProduct("AFR.ShoutcastBridge")]
[assembly: AssemblyCopyright("Copyright © Hewlett-Packard 2014")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Durch Festlegen von ComVisible auf "false" werden die Typen in dieser Assembly unsichtbar
// für COM-Komponenten. Wenn Sie auf einen Typ in dieser Assembly von
// COM zugreifen müssen, legen Sie das ComVisible-Attribut für diesen Typ auf "true" fest.
[assembly: ComVisible(false)]
// Die folgende GUID bestimmt die ID der Typbibliothek, wenn dieses Projekt für COM verfügbar gemacht wird
[assembly: Guid("bf523a9d-260e-4949-bbdd-0c0acccc84da")]
// Versionsinformationen für eine Assembly bestehen aus den folgenden vier Werten:
//
// Hauptversion
// Nebenversion
// Buildnummer
// Revision
//
// Sie können alle Werte angeben oder die standardmäßigen Build- und Revisionsnummern
// übernehmen, indem Sie "*" eingeben:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@ -0,0 +1,4 @@
namespace AFR.ShoutcastBridge
{
public delegate string ShoutcastAuthenticationEventHandler(string password);
}

View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{491CCFC0-2C51-451F-92AE-056B0474AD7E}</ProjectGuid>
<OutputType>Exe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>AFR.ShoutcastBridge</RootNamespace>
<AssemblyName>sc_bridge</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<TargetFrameworkProfile />
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\..\</SolutionDir>
<RestorePackages>true</RestorePackages>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup>
<OutDir>$(SolutionDir)\bin\$(Configuration)\$(Platform)\</OutDir>
<IntDir>$(SolutionDir)\obj\$(TargetName)\$(Configuration)\$(Platform)\</IntDir>
<IntermediateOutputPath>$(SolutionDir)\obj\$(TargetName)\$(Configuration)\$(Platform)\</IntermediateOutputPath>
<BaseIntermediateOutputPath>$(SolutionDir)\obj\$(TargetName)\$(Configuration)\$(Platform)\</BaseIntermediateOutputPath>
<OutputPath>$(SolutionDir)\bin\$(Configuration)\$(Platform)\</OutputPath>
</PropertyGroup>
<ItemGroup>
<Reference Include="log4net, Version=1.2.13.0, Culture=neutral, PublicKeyToken=669e0ddf0bb1aa2a, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\log4net.2.0.3\lib\net40-full\log4net.dll</HintPath>
</Reference>
<Reference Include="Newtonsoft.Json, Version=6.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\Newtonsoft.Json.6.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Web" />
<Reference Include="System.XML" />
<Reference Include="System.Xml.Linq" />
<Reference Include="uhttpsharp">
<HintPath>..\packages\uHttpSharp.0.1.4.7\lib\net40\uhttpsharp.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="AdminHandler.cs" />
<Compile Include="IcecastWriter.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ShoutcastAuthenticationEventHandler.cs" />
<Compile Include="ShoutcastBridgeServer.cs" />
<Compile Include="ShoutcastBridgeMountpoint.cs" />
<Compile Include="ShoutcastDataEventHandler.cs" />
<Compile Include="ShoutcastDisconnectEventHandler.cs" />
<Compile Include="ShoutcastReadingStream.cs" />
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<Content Include="Config.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

View File

@ -0,0 +1,11 @@
namespace AFR.ShoutcastBridge
{
public class ShoutcastBridgeMountpoint
{
public string IcecastHost { get; set; }
public ushort IcecastPort { get; set; }
public string IcecastMountpoint { get; set; }
public string IcecastUsername { get; set; }
public string IcecastPassword { get; set; }
}
}

View File

@ -0,0 +1,227 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
namespace AFR.ShoutcastBridge
{
public class ShoutcastBridgeServer
{
private readonly ILog _log;
private readonly ILog _adminlog;
private readonly ILog _sourcelog;
private TcpListener _sourceServer;
private HttpServer _adminServer;
private readonly IPAddress _ip;
private readonly ushort _port;
private readonly Dictionary<string, ShoutcastBridgeMountpoint> _mountpoints =
new Dictionary<string, ShoutcastBridgeMountpoint>();
private readonly Dictionary<string, Tuple<ShoutcastReadingStream, IcecastWriter>> _connectedMountpoints =
new Dictionary<string, Tuple<ShoutcastReadingStream, IcecastWriter>>();
public ShoutcastBridgeServer(IPAddress ip, ushort port)
{
_ip = ip;
_port = port;
_log = LogManager.GetLogger("ShoutcastBridge");
_adminlog = LogManager.GetLogger("Shout1Admin");
_sourcelog = LogManager.GetLogger("Shout1Source");
}
public void Start()
{
_log.DebugFormat("Starting SCv1 source server on port {0}...", _port + 1);
_sourceServer = new TcpListener(_ip, _port + 1);
try
{
_sourceServer.Start();
}
catch (Exception error)
{
_log.ErrorFormat("Could not start up source server. {0}", error.Message);
}
_log.DebugFormat("Starting SC admin HTTP server on port {0}...", _port);
_adminServer = new HttpServer(new HttpRequestProvider());
try
{
_adminServer.Use(new TcpListenerAdapter(new TcpListener(_ip, _port)));
}
catch (Exception error)
{
_log.ErrorFormat("Could not start up admin HTTP server. {0}", error.Message);
}
_adminServer.Use(new AdminHandler(this));
try
{
_adminServer.Start();
}
catch (Exception error)
{
_log.ErrorFormat("Could not start up admin HTTP server. {0}", error.Message);
}
_log.Info("Shoutcast bridge started up successfully, now ready.");
while (true)
{
Socket sock;
try
{
sock = _sourceServer.AcceptSocket();
}
catch
{
continue;
}
_log.Debug("Accepting incoming connection");
var ns = new NetworkStream(sock, true);
var srs = new ShoutcastReadingStream((IPEndPoint)sock.RemoteEndPoint, ns);
srs.Authenticating += password =>
{
if (!_mountpoints.ContainsKey(password))
{
_sourcelog.DebugFormat("[{0}] Connection declined: Invalid password", srs.ClientEndPoint);
return "Invalid password";
}
if (_connectedMountpoints.ContainsKey(password))
{
_sourcelog.DebugFormat("[{0}] Connection declined: Stream already in use", srs.ClientEndPoint);
return "Stream already in use";
}
var mp = _mountpoints[password];
var writer = new IcecastWriter()
{
Hostname = mp.IcecastHost,
Port = mp.IcecastPort,
Mountpoint = mp.IcecastMountpoint,
Username = string.IsNullOrEmpty(mp.IcecastUsername) ? "source" : mp.IcecastUsername,
Password = mp.IcecastPassword,
ContentType = "undefined" /* will be defined on first packet */
};
_connectedMountpoints.Add(password, new Tuple<ShoutcastReadingStream, IcecastWriter>(srs, writer));
_sourcelog.DebugFormat("[{0}] Testing connection to master server...", srs.ClientEndPoint);
if (!writer.Open())
{
_sourcelog.DebugFormat("[{0}] Connection declined: Master server was unavailable", srs.ClientEndPoint);
return "Master server unavailable, try again later.";
}
writer.Close();
_sourcelog.DebugFormat("[{0}] Connection accepted", srs.ClientEndPoint);
return "OK2";
};
srs.Disconnected += () =>
{
_sourcelog.DebugFormat("[{0}] Disconnected", srs.ClientEndPoint);
var pws = _connectedMountpoints.Where(s => s.Value.Item1 == srs).Select(s => s.Key).ToArray();
if (!pws.Any())
{
_sourcelog.ErrorFormat("[{0}] Could not find any connected mountpoints associated with this IP, dangling connection! This is a bug!", srs.ClientEndPoint);
}
foreach (var pw in pws.ToArray())
{
_connectedMountpoints[pw].Item2.Close(); // close Icecast connection
_connectedMountpoints.Remove(pw); // remove from registered connections
}
};
srs.ReceivedData += data =>
{
var conns = _connectedMountpoints.Where(s => s.Value.Item1 == srs).ToArray();
if (!conns.Any())
{
_sourcelog.ErrorFormat("Dangling source connection from {0} - nothing bound to it!", srs.ClientEndPoint);
}
foreach (var conn in conns)
{
var icecast = conn.Value.Item2;
var shoutcast = conn.Value.Item1;
if (!icecast.Connected)
{
_sourcelog.InfoFormat("[{0}] Connecting to {1}:{2}{3}...", srs.ClientEndPoint, icecast.Hostname, icecast.Port, icecast.Mountpoint);
icecast.Name = icecast.Description = shoutcast.StreamName;
icecast.Genre = shoutcast.StreamGenre;
icecast.KBitrate = shoutcast.StreamKBitrate;
icecast.Url = shoutcast.StreamUrl;
icecast.Public = shoutcast.StreamPublic;
if (icecast.ContentType == "undefined")
switch (BitConverter.ToString(data.Take(2).ToArray()).Replace("-", "").ToLower())
{
case "4f67": // OGG container
_sourcelog.DebugFormat("[{0}] Detected OGG audio container", srs.ClientEndPoint);
icecast.ContentType = "audio/ogg";
break;
case "fff9": // AAC
_sourcelog.DebugFormat("[{0}] Detected AAC-LC data", srs.ClientEndPoint);
icecast.ContentType = "audio/aac";
break;
default:
_sourcelog.DebugFormat("[{0}] Assuming MP3 codec", srs.ClientEndPoint);
icecast.ContentType = "audio/mpeg";
break;
}
if (!icecast.Open())
{
_sourcelog.ErrorFormat("[{0}] Could not connect with Icecast, retrying on next packet", srs.ClientEndPoint);
continue;
}
_sourcelog.InfoFormat("[{0}] Connected!", srs.ClientEndPoint);
// TODO: Sync metadata after connection immediately!
}
icecast.Push(data);
}
};
srs.Start();
}
}
public void Stop()
{
_adminServer.Dispose();
_sourceServer.Stop();
}
public void AddMountpoint(string password, ShoutcastBridgeMountpoint mp)
{
_log.DebugFormat("Adding mountpoint: {0}:{1}, mountpoint \"{2}\"", mp.IcecastHost, mp.IcecastPort, mp.IcecastMountpoint);
_mountpoints.Add(password, mp);
}
public bool UpdateMetadata(IPEndPoint remoteEndPoint, string password, string song)
{
// Is stream connected?
if (!_connectedMountpoints.ContainsKey(password))
{
_adminlog.DebugFormat("[{0}] Metadata update declined: Invalid password", remoteEndPoint);
return false;
}
// Authorize client - requirement: same IP
if (!_connectedMountpoints[password].Item1.ClientEndPoint.Address.Equals(remoteEndPoint.Address))
{
_adminlog.DebugFormat("[{0}] Metadata update declined: IP mismatch", remoteEndPoint);
return false;
}
// Finally update metadata
_adminlog.DebugFormat("[{0}] Metadata update: {1}", remoteEndPoint, song);
return _connectedMountpoints[password].Item2.SendMetadata(song);
}
}
}

View File

@ -0,0 +1,4 @@
namespace AFR.ShoutcastBridge
{
public delegate void ShoutcastDataEventHandler(byte[] data);
}

View File

@ -0,0 +1,4 @@
namespace AFR.ShoutcastBridge
{
public delegate void ShoutcastDisconnectEventHandler();
}

View File

@ -0,0 +1,148 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
namespace AFR.ShoutcastBridge
{
public class ShoutcastReadingStream
{
private readonly NetworkStream _ns;
private readonly StreamReader _sr;
private readonly StreamWriter _sw;
private CancellationTokenSource _cancel;
private Task _task;
private readonly Dictionary<string, string> _headers = new Dictionary<string, string>();
public string StreamName
{
get { return _headers.ContainsKey("icy-name") ? _headers["icy-name"] : string.Empty; }
}
public string StreamGenre
{
get { return _headers.ContainsKey("icy-genre") ? _headers["icy-genre"] : string.Empty; }
}
public bool StreamPublic
{
get { return _headers.ContainsKey("icy-pub") && int.Parse(_headers["icy-pub"]) >= 1; }
}
public ushort StreamKBitrate
{
get { return _headers.ContainsKey("icy-br") ? ushort.Parse(_headers["icy-br"]) : (ushort)0; }
}
public string StreamUrl
{
get { return _headers.ContainsKey("icy-url") ? _headers["icy-url"] : string.Empty; }
}
public string StreamIrc
{
get { return _headers.ContainsKey("icy-irc") ? _headers["icy-irc"] : string.Empty; }
}
public IPEndPoint ClientEndPoint { get; private set; }
public ShoutcastReadingStream(IPEndPoint ep, NetworkStream ns)
{
ClientEndPoint = ep;
_ns = ns;
_task = null;
_sr = new StreamReader(ns);
_sw = new StreamWriter(ns) {AutoFlush = true};
}
public void Start()
{
_cancel = new CancellationTokenSource();
_task = Task.Factory.StartNew(_start, _cancel.Token).ContinueWith(task =>
{
if (Disconnected != null)
Disconnected();
});
}
private void _start()
{
var password = _sr.ReadLine();
// Check if any password given
if (password == null)
return;
// Check if password correct
password = password.TrimEnd('\n', '\r');
var status = Authenticating(password);
if (Authenticating != null && status != "OK2")
{
_sw.WriteLine(status);
_ns.Dispose();
return;
}
_sw.WriteLine(status);
// Headers
var line = _sr.ReadLine();
while (line != null && line.Trim().Any())
{
var lineSplit = line.Split(':');
if (lineSplit.Length < 2)
continue; // Invalid header line
var name = lineSplit[0];
var value = string.Join(":", lineSplit.Skip(1));
if (_headers.ContainsKey(name))
_headers[name] = value;
else
_headers.Add(name, value);
line = _sr.ReadLine();
}
// Content
var data = new byte[2048];
try
{
int length;
while ((length = _ns.Read(data, 0, data.Length)) > 0)
{
_cancel.Token.ThrowIfCancellationRequested();
if (ReceivedData != null)
ReceivedData(data.Take(length).ToArray());
}
}
catch
{
LogManager.GetLogger("ShoutRead").Warn("Exception in reading loop while reading. Interpreting as disconnection.");
}
}
public void Stop()
{
if (_cancel == null || _task == null)
return;
_cancel.Cancel();
_task.Wait();
_task = null;
}
~ShoutcastReadingStream()
{
_sr.Dispose();
_sw.Dispose();
}
public event ShoutcastDisconnectEventHandler Disconnected;
public event ShoutcastAuthenticationEventHandler Authenticating;
public event ShoutcastDataEventHandler ReceivedData;
}
}

11
src/sc_bridge/app.config Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.5.0.0" newVersion="4.5.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" /></startup></configuration>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="log4net" version="2.0.3" targetFramework="net45" />
<package id="Newtonsoft.Json" version="6.0.3" targetFramework="net45" />
<package id="uHttpSharp" version="0.1.4.7" targetFramework="net40" />
</packages>