2025-09-02 01:58:53 -07:00
using Microsoft.Extensions.Logging ;
using OwlCore.AbstractUI.Models ;
2024-05-20 23:43:21 -05:00
using OwlCore.ComponentModel ;
2021-04-25 22:59:36 -05:00
using System ;
using System.Collections.Generic ;
2025-09-01 22:51:58 -07:00
using System.Diagnostics ;
2021-04-25 22:59:36 -05:00
using System.IO ;
2025-09-02 00:06:27 -07:00
using System.Linq ;
using System.Net ;
2024-05-17 23:42:46 -05:00
using System.Net.Http ;
2025-09-01 22:51:58 -07:00
using System.Reflection.PortableExecutable ;
using System.Text ;
2025-09-02 00:06:27 -07:00
using System.Text.RegularExpressions ;
2024-05-17 23:42:46 -05:00
using System.Threading ;
2021-04-25 22:59:36 -05:00
using System.Threading.Tasks ;
2025-09-02 00:27:40 -07:00
using ZuneModCore.Services ;
2021-04-25 22:59:36 -05:00
using static ZuneModCore . Mods . FeaturesOverrideMod ;
2024-05-20 21:53:52 -05:00
namespace ZuneModCore.Mods ;
2024-05-21 20:54:02 -05:00
public class WebservicesModFactory : DIModFactoryBase < WebservicesMod >
{
2025-09-03 19:48:41 -04:00
public override ModMetadata Metadata { get ; } = new ()
{
Id = nameof ( WebservicesMod ),
Title = "Community Webservices" ,
Author = "Joshua \"Yoshi\" Askharoun" ,
Version = new ( 1 , 2 ),
Description = "Partially restores online features such as the Marketplace by patching the Zune desktop software " +
"to use the community's recreation of the original Zune servers." ,
};
2024-05-21 20:54:02 -05:00
}
2025-09-02 01:58:53 -07:00
public partial class WebservicesMod ( ModMetadata metadata , IModCoreConfig modConfig , ILogger < WebservicesMod > log ) : Mod ( metadata ), IAsyncInit
2021-04-25 22:59:36 -05:00
{
2024-05-20 21:53:52 -05:00
private const int ZUNESERVICES_ENDPOINTS_BLOCK_OFFSET = 0x14D60 ;
private const int ZUNESERVICES_ENDPOINTS_BLOCK_LENGTH = 0x884 ;
private const string WEBSERVICE_ICON_TESTING = "\uE72C" ;
private const string WEBSERVICE_ICON_SUCCESS = "\uE73E" ;
private const string WEBSERVICE_ICON_UNREACHABLE = "\uF384" ;
private const string WEBSERVICE_ICON_UNAVAILABLE = "\uE894" ;
private const string WEBSERVICE_SUBTITLE_TESTING = "Testing..." ;
private const string WEBSERVICE_SUBTITLE_SUCCESS = "OK" ;
private const string WEBSERVICE_SUBTITLE_UNREACHABLE = "Unreachable" ;
private static readonly string [] _affectedFeatures =
[
"Apps", "Channels", "Games", "Marketplace",
"MBRPreview", "MBRPurchase", "MBRRental",
"Music", "MusicVideos", "Picks", "Podcasts",
"Sign In Available", "Social", "Videos",
"SocialMarketplace", "SubscriptionFreeTracks",
] ;
2025-09-02 00:06:27 -07:00
private static readonly ( string description , string subdomain )[] _knownSubdomains =
[
("Main website", "www"),
("Social website", "social"),
("Catalog", "catalog"),
("Image catalog", "image.catalog"),
("Social", "socialapi"),
("Social [Comments] ", " comments "),
( "Social [Inbox]" , "inbox" ),
( "Commerce [Sign in]" , "commerce" ),
( "Mix" , "mix" ),
( "Resources [Firmware]" , "resources" ),
( "Statistics" , "stats" ),
];
2024-05-20 21:53:52 -05:00
private HttpClient _client ;
private CancellationTokenSource _cts = new ();
2025-09-02 00:06:27 -07:00
private string _methodFile ;
2024-05-20 21:53:52 -05:00
2025-09-02 00:26:45 -07:00
public string ZuneInstallDir => modConfig . ZuneInstallDir ;
2024-05-20 21:53:52 -05:00
public override AbstractUICollection ? GetDefaultOptionsUI ()
2021-04-25 22:59:36 -05:00
{
2024-05-20 21:53:52 -05:00
AbstractUICollection optionsUi = new ( nameof ( FeaturesOverrideMod ))
2021-08-04 23:02:07 -05:00
{
2024-05-20 21:53:52 -05:00
new AbstractTextBox ( "hostBox" , string . Empty , "zune.net" )
2021-08-04 23:02:07 -05:00
{
2024-05-20 21:53:52 -05:00
Title = "host" ,
TooltipText = "The host where the replacement servers are located. Must be the same length as \"zune.net\"."
},
2025-09-02 00:06:27 -07:00
new AbstractDataList ( "hostsTest" ,
_knownSubdomains . Select ( e => GetWebserviceAvailabilityUI ( e . description , e . subdomain )))
2024-05-20 21:53:52 -05:00
{
Subtitle = "TEST AVAILABILITY OF EACH KNOWN WEB SERVICE ON THE NEW HOST." . ToUpper (),
IsUserEditingEnabled = false ,
PreferredDisplayMode = AbstractDataListPreferredDisplayMode . Grid ,
}
};
return optionsUi ;
}
private static AbstractUIMetadata GetWebserviceAvailabilityUI ( string name , string subDomain )
{
return new ( "serviceTestUI_" + subDomain )
{
Title = name ,
IconCode = WEBSERVICE_ICON_TESTING
};
}
public override IReadOnlyList < Type >? DependentMods => null ;
2024-05-20 23:43:21 -05:00
public bool IsInitialized { get ; private set ; }
public Task InitAsync ( CancellationToken token = default )
2024-05-20 21:53:52 -05:00
{
2024-05-20 23:43:21 -05:00
if ( IsInitialized )
return Task . CompletedTask ;
2025-09-02 00:06:27 -07:00
_methodFile = Path . Combine ( StorageDirectory , "AppliedMethod.txt" );
2024-05-20 21:53:52 -05:00
_client = new ()
{
Timeout = TimeSpan . FromSeconds ( 3 )
};
AbstractTextBox newHostBox = ( AbstractTextBox ) OptionsUI ![ 0 ];
newHostBox . ValueChanged += OnHostChanged ;
newHostBox . Value = "zunes.me" ;
2024-05-20 23:43:21 -05:00
IsInitialized = true ;
2024-05-20 21:53:52 -05:00
return Task . CompletedTask ;
}
public override async Task < string? > Apply ()
2025-09-01 22:55:01 -07:00
{
var newHost = (( AbstractTextBox ) OptionsUI ![ 0 ]). Value ;
2025-09-02 01:09:58 -07:00
var errorMessage = await PatchZuneServices ( newHost );
2025-09-01 22:55:01 -07:00
if ( errorMessage is not null )
{
2025-09-02 01:58:53 -07:00
log . LogWarning ( "Binary patching failed, falling back to hosts file modification: {ErrorMessage}" , errorMessage );
2025-09-02 00:06:27 -07:00
// Fallback to editing hosts file
errorMessage = await AddEntriesToHostsFile ( newHost );
if ( errorMessage is null )
await RecordAppliedMethod ( ApplicationMethod . HostsEntries );
}
else
{
await RecordAppliedMethod ( ApplicationMethod . BinaryPatch );
2025-09-01 22:55:01 -07:00
}
return errorMessage ;
}
public override async Task < string? > Reset ()
{
string zsDllPath = Path . Combine ( ZuneInstallDir , "ZuneService.dll" );
2025-09-02 00:06:27 -07:00
2025-09-01 22:55:01 -07:00
try
{
2025-09-02 00:06:27 -07:00
var method = await ReadAppliedMethod ();
switch ( method )
{
case ApplicationMethod . BinaryPatch :
// Restore backup to application folder
File . Copy ( Path . Combine ( StorageDirectory , "ZuneService.original.dll" ), zsDllPath , true );
break ;
case ApplicationMethod . HostsEntries :
// TODO: Remove entries from hosts file
return $"Automatic removal of hosts file entries is not yet supported. Please manually remove any entries for zune.net and its subdomains from your hosts file." ;
break ;
}
// Clear application method file
try
{
File . Delete ( _methodFile );
}
catch ( FileNotFoundException ) { }
2025-09-01 22:55:01 -07:00
// Disable all feature overrides affected by new servers
bool setOverrideSuccess = true ;
foreach ( var feature in _affectedFeatures )
setOverrideSuccess &= SetFeatureOverride ( feature , false );
if ( setOverrideSuccess != true )
return "Unable to reset feature overrides. The mod was successfully removed, but you may still be able to see it in the Zune software." ;
return null ;
}
catch ( IOException )
{
return $"Unable to replace '{zsDllPath}'. Verify that the Zune software is not running and try again." ;
}
catch ( Exception ex )
{
return ex . Message ;
}
}
2025-09-02 00:06:27 -07:00
2025-09-01 22:55:01 -07:00
private async Task < string? > PatchZuneServices ( string newHost )
2024-05-20 21:53:52 -05:00
{
// Verify that ZuneServices.dll exists
FileInfo zsDllInfo = new ( Path . Combine ( ZuneInstallDir , "ZuneService.dll" ));
if (! zsDllInfo . Exists )
{
return $"The file '{zsDllInfo.FullName}' does not exist." ;
2021-08-05 20:45:26 -05:00
}
2021-04-25 22:59:36 -05:00
2024-05-20 21:53:52 -05:00
// Make a backup if it doesn't already exist
FileInfo zsDllBackupInfo = new ( Path . Combine ( StorageDirectory , "ZuneService.original.dll" ));
if (! zsDllBackupInfo . Exists )
2021-11-18 01:17:39 -06:00
{
2024-05-20 21:53:52 -05:00
File . Copy ( zsDllInfo . FullName , zsDllBackupInfo . FullName , true );
2021-11-18 01:17:39 -06:00
}
2024-05-20 21:53:52 -05:00
try
2021-11-18 01:17:39 -06:00
{
2025-08-19 17:49:53 -05:00
// Get and validate replacement host
2025-09-01 22:51:58 -07:00
var oldHost = "zune.net" ;
2025-08-19 17:49:53 -05:00
if ( newHost . Length != oldHost . Length )
{
2025-09-01 22:51:58 -07:00
return $"The new host \" { newHost } \ " must have the same length as \"{oldHost}\"." ;
2025-08-19 17:49:53 -05:00
}
2025-09-01 22:51:58 -07:00
var versionInfo = FileVersionInfo . GetVersionInfo ( zsDllInfo . FullName );
if ( versionInfo ?. FileVersion is null )
2025-08-19 17:49:53 -05:00
{
2025-09-01 22:51:58 -07:00
return $"Failed to get version info for '{zsDllInfo.FullName}'." ;
2025-08-19 17:49:53 -05:00
}
2025-09-01 22:51:58 -07:00
var fileVersion = new Version ( versionInfo . ProductMajorPart , versionInfo . ProductMinorPart ,
versionInfo . ProductBuildPart , versionInfo . FilePrivatePart );
2024-05-20 21:53:52 -05:00
2025-09-01 22:51:58 -07:00
// Open the file and determine the architecture
2025-09-02 01:09:58 -07:00
using FileStream zsDll = zsDllInfo . Open ( FileMode . Open );
2025-09-01 22:51:58 -07:00
Machine architecture ;
using ( PEReader peReader = new ( zsDll , PEStreamOptions . LeaveOpen | PEStreamOptions . PrefetchMetadata ))
architecture = peReader . PEHeaders . CoffHeader . Machine ;
// Select appropriate patch based on version and architecture
if ( fileVersion == new Version ( 4 , 8 , 2345 , 0 ) && architecture is Machine . Amd64 )
2024-05-18 00:59:50 -05:00
{
2025-09-01 22:51:58 -07:00
var patchResult = Patch48Version64Bit ( zsDll , oldHost , newHost );
if ( patchResult is not null )
return patchResult ;
2024-05-20 21:53:52 -05:00
}
2025-09-01 22:51:58 -07:00
else
2024-10-22 14:04:42 -05:00
{
2025-09-01 22:51:58 -07:00
return $"This mod does not support Zune {fileVersion} on {architecture}." ;
2024-10-22 14:04:42 -05:00
}
2025-09-01 22:51:58 -07:00
await zsDll . DisposeAsync ();
2024-05-20 21:53:52 -05:00
// Enable all feature overrides affected by new servers
2025-09-01 22:51:58 -07:00
var setOverrideSuccess = true ;
2024-05-20 21:53:52 -05:00
foreach ( var feature in _affectedFeatures )
setOverrideSuccess &= SetFeatureOverride ( feature , true );
if ( setOverrideSuccess != true )
return "Unable to set feature overrides. The mod was successful, but you may not be able to see it in the Zune software." ;
return null ;
}
catch ( IOException )
{
return $"Unable to replace '{zsDllInfo.FullName}'. Verify that the Zune software is not running and try again." ;
}
catch ( Exception ex )
{
return ex . Message ;
}
}
2025-09-01 22:55:01 -07:00
private static string? Patch48Version64Bit ( Stream zsDll , string oldHost , string newHost )
2024-05-20 21:53:52 -05:00
{
2025-09-01 22:55:01 -07:00
using BinaryWriter zsDllWriter = new ( zsDll );
using BinaryReader zsDllReader = new ( zsDll );
// Read URL block as string
zsDllReader . BaseStream . Position = ZUNESERVICES_ENDPOINTS_BLOCK_OFFSET ;
var originalEndpointBytes = zsDllReader . ReadBytes ( ZUNESERVICES_ENDPOINTS_BLOCK_LENGTH );
var endpointText = Encoding . Unicode . GetString ( originalEndpointBytes );
// Try to determine previous host
2024-05-20 21:53:52 -05:00
try
{
2025-09-01 22:55:01 -07:00
var firstUrl = endpointText [.. endpointText . IndexOf ( '\0' )];
oldHost = firstUrl . Substring ( 11 , oldHost . Length );
2024-05-20 21:53:52 -05:00
}
2025-09-01 22:55:01 -07:00
catch { }
// Patch ZuneServices.dll to use the new host instead of zune.net
endpointText = endpointText . Replace ( oldHost , newHost );
var patchedEndpointBytes = Encoding . Unicode . GetBytes ( endpointText );
if ( patchedEndpointBytes . Length != originalEndpointBytes . Length )
2024-05-20 21:53:52 -05:00
{
2025-09-01 22:55:01 -07:00
return "Failed to safely overwrite strings in DLL." ;
2024-05-20 21:53:52 -05:00
}
2025-09-01 22:55:01 -07:00
zsDllWriter . Seek ( ZUNESERVICES_ENDPOINTS_BLOCK_OFFSET , SeekOrigin . Begin );
zsDllWriter . Write ( patchedEndpointBytes );
return null ;
2024-05-20 21:53:52 -05:00
}
2025-09-02 00:06:27 -07:00
private static async Task < string? > AddEntriesToHostsFile ( string newHost )
{
var hostEntry = await Dns . GetHostEntryAsync ( newHost );
var ipAddress = hostEntry ?. AddressList ?. FirstOrDefault ();
if ( ipAddress is null )
{
return $"Failed to resolve host '{newHost}' to an IP address." ;
}
var hostsPath = Path . Combine ( Environment . GetFolderPath ( Environment . SpecialFolder . System ), @"drivers\etc\hosts" );
var hostLines = ( await File . ReadAllLinesAsync ( hostsPath )). ToList ();
var domains = _knownSubdomains
. Select ( e => GetDomainName ( e . subdomain , "zune.net" ))
. ToHashSet ();
// Update existing entries
Regex rx = HostsEntryRegex ();
for ( int l = 0 ; l < hostLines . Count ; l ++)
{
var line = hostLines [ l ]. Trim ();
// Ignore comments and empty lines
if ( line . Length <= 0 || line [ 0 ] is '#' )
continue ;
var match = rx . Match ( line );
if (! match . Success )
continue ;
var domain = match . Groups [ "name" ]. Value ;
// Ignore entries unrelated to Zune
if (! domain . EndsWith ( "zune.net" ))
continue ;
// Reconstruct entry with new IP address
hostLines [ l ] = $"{ipAddress} {domain}" ;
// Track that we handled this domain
domains . Remove ( domain );
}
// Add any entries that didn't already exist
if ( domains . Count > 0 )
{
foreach ( var domain in domains )
hostLines . Add ( $"{ipAddress} {domain}" );
}
await File . WriteAllLinesAsync ( hostsPath , hostLines );
return null ;
}
2024-05-20 21:53:52 -05:00
async void OnHostChanged ( object? sender , string newHost )
{
await _cts . CancelAsync ();
_client . CancelPendingRequests ();
_cts = new ();
AbstractDataList list = ( AbstractDataList ) OptionsUI ![ 1 ];
// Reset all statuses
foreach ( AbstractUIMetadata metadata in list . Items )
{
metadata . IconCode = WEBSERVICE_ICON_TESTING ;
metadata . Subtitle = WEBSERVICE_SUBTITLE_TESTING ;
2021-11-18 01:17:39 -06:00
}
2024-05-20 21:53:52 -05:00
// We want to update all the statuses, but we also need to be able
// to cancel it when the user enters a new host.
foreach ( AbstractUIMetadata metadata in list . Items )
UpdateStatus ( metadata , newHost , _cts . Token );
}
private async Task < string? > Ping ( string url , CancellationToken token = default )
{
try
2021-04-25 22:59:36 -05:00
{
2024-05-20 21:53:52 -05:00
var response = await _client . GetAsync ( url , token );
response . EnsureSuccessStatusCode ();
return null ;
2021-04-25 22:59:36 -05:00
}
2024-05-20 21:53:52 -05:00
catch ( HttpRequestException webEx )
2021-04-25 22:59:36 -05:00
{
2024-05-20 21:53:52 -05:00
if ( webEx . InnerException ?. InnerException != null )
return webEx . InnerException . InnerException . Message ;
return webEx ?. Message ?? string . Empty ;
2021-04-25 22:59:36 -05:00
}
2024-05-20 21:53:52 -05:00
catch ( Exception ex )
2022-01-02 21:30:16 -06:00
{
2024-05-20 21:53:52 -05:00
return ex ?. Message ?? string . Empty ;
2022-01-02 21:30:16 -06:00
}
2024-05-20 21:53:52 -05:00
}
2022-01-02 21:30:16 -06:00
2024-05-20 21:53:52 -05:00
private async Task UpdateStatus ( AbstractUIMetadata metadata , string newHost , CancellationToken token = default )
{
2025-09-02 00:06:27 -07:00
string url = "http://" + GetDomainName ( metadata . Id . Split ( '_' )[ 1 ], newHost );
2024-05-20 21:53:52 -05:00
string? pingResult = await Ping ( url , token );
if ( pingResult == null )
2022-01-02 21:30:16 -06:00
{
2024-05-20 21:53:52 -05:00
metadata . IconCode = WEBSERVICE_ICON_SUCCESS ;
metadata . Subtitle = WEBSERVICE_SUBTITLE_SUCCESS ;
2022-01-02 21:30:16 -06:00
}
2024-05-20 21:53:52 -05:00
else if ( pingResult != string . Empty )
2024-05-17 23:42:46 -05:00
{
2024-05-20 21:53:52 -05:00
metadata . IconCode = WEBSERVICE_ICON_UNAVAILABLE ;
metadata . Subtitle = pingResult ;
}
else
{
metadata . IconCode = WEBSERVICE_ICON_UNREACHABLE ;
metadata . Subtitle = WEBSERVICE_SUBTITLE_UNREACHABLE ;
2024-05-17 23:42:46 -05:00
}
2021-04-25 22:59:36 -05:00
}
2025-09-02 00:06:27 -07:00
private static string GetDomainName ( string subdomain , string host )
{
return $"{subdomain}.{host}" . Replace ( "www." , string . Empty );
}
private async Task RecordAppliedMethod ( ApplicationMethod method )
{
await File . WriteAllTextAsync ( _methodFile , method . ToString ());
}
private async Task < ApplicationMethod > ReadAppliedMethod ()
{
if (! File . Exists ( _methodFile ))
return ApplicationMethod . None ;
var methodText = await File . ReadAllTextAsync ( _methodFile );
return Enum . Parse < ApplicationMethod >( methodText );
}
[GeneratedRegex(@"(?<ip>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s+(?<name>[\w.-] +?)( \ s +| $ ) ")]
private static partial Regex HostsEntryRegex ();
2025-09-08 11:53:57 -05:00
private enum ApplicationMethod { None , BinaryPatch , HostsEntries }
2021-04-25 22:59:36 -05:00
}