forked from serverkomplex/ShoutcastBridge
Initial commit
commit
f47b51127c
|
@ -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/
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<solution>
|
||||
<add key="disableSourceControlIntegration" value="true" />
|
||||
</solution>
|
||||
</configuration>
|
Binary file not shown.
|
@ -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>
|
|
@ -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
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 `-`
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")]
|
|
@ -0,0 +1,4 @@
|
|||
namespace AFR.ShoutcastBridge
|
||||
{
|
||||
public delegate string ShoutcastAuthenticationEventHandler(string password);
|
||||
}
|
|
@ -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>
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
namespace AFR.ShoutcastBridge
|
||||
{
|
||||
public delegate void ShoutcastDataEventHandler(byte[] data);
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
namespace AFR.ShoutcastBridge
|
||||
{
|
||||
public delegate void ShoutcastDisconnectEventHandler();
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
Loading…
Reference in New Issue