Use Classicube's api rather than web scraping.

This commit is contained in:
UnknownShadow200 2015-11-03 20:31:34 +11:00
parent f69596db27
commit 887d846ef2
10 changed files with 138 additions and 128 deletions

View file

@ -109,7 +109,8 @@ namespace Launcher2 {
}
void ConnectToServer( int mouseX, int mouseY ) {
game.ConnectToServer( Get( 3 ) );
LauncherTableWidget table = (LauncherTableWidget)widgets[tableIndex];
game.ConnectToServer( table.servers, Get( 3 ) );
}
void MouseWheelChanged( object sender, MouseWheelEventArgs e ) {

View file

@ -37,7 +37,7 @@ namespace Launcher2 {
TableEntry entry = usedEntries[i];
if( mouseY >= entry.Y && mouseY < entry.Y + entry.Height ) {
if( lastIndex == i && (DateTime.UtcNow - lastPress).TotalSeconds > 1 ) {
Window.ConnectToServer( entry.Hash );
Window.ConnectToServer( servers, entry.Hash );
lastPress = DateTime.UtcNow;
}
SelectedChanged( entry.Hash );

View file

@ -15,9 +15,11 @@ namespace Launcher2 {
public Action<string> SelectedChanged;
TableEntry[] entries, usedEntries;
internal List<ServerListEntry> servers;
public void SetEntries( List<ServerListEntry> servers ) {
entries = new TableEntry[servers.Count];
usedEntries = new TableEntry[servers.Count];
this.servers = servers;
int index = 0;
foreach( ServerListEntry e in servers ) {

View file

@ -4,7 +4,7 @@
<ProjectGuid>{3E84ACC1-27B4-401B-A359-6AAE4DF6C9B5}</ProjectGuid>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<OutputType>WinExe</OutputType>
<OutputType>Exe</OutputType>
<RootNamespace>Launcher2</RootNamespace>
<AssemblyName>Launcher2</AssemblyName>
<TargetFrameworkVersion>v2.0</TargetFrameworkVersion>

View file

@ -82,8 +82,20 @@ namespace Launcher2 {
screen.Init();
}
public bool ConnectToServer( string hash ) {
public bool ConnectToServer( List<ServerListEntry> publicServers, string hash ) {
if( String.IsNullOrEmpty( hash ) ) return false;
ClientStartData data = null;
foreach( ServerListEntry entry in publicServers ) {
if( entry.Hash == hash ) {
data = new ClientStartData( Session.Username, entry.Mppass,
entry.IPAddress, entry.Port );
Client.Start( data, true );
return true;
}
}
// Fallback to private server handling
try {
data = Session.GetConnectInfo( hash );
} catch( WebException ex ) {

View file

@ -17,20 +17,21 @@
// <website>https://github.com/facebook-csharp-sdk/simple-json</website>
//-----------------------------------------------------------------------
// original json parsing code from http://techblog.procurios.nl/k/618/news/view/14605/14863/How-do-I-write-my-own-parser-for-JSON.html
// This is a significantly cutdown version of the original simple-json repository.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
namespace SimpleJson {
namespace Launcher2 {
public static class SimpleJson {
public static class Json {
const int TOKEN_NONE = 0, TOKEN_CURLY_OPEN = 1, TOKEN_CURLY_CLOSE = 2;
const int TOKEN_SQUARED_OPEN = 3, TOKEN_SQUARED_CLOSE = 4, TOKEN_COLON = 5;
const int TOKEN_COMMA = 6, TOKEN_STRING = 7, TOKEN_NUMBER = 8;
const int TOKEN_TRUE = 9, TOKEN_FALSE = 10, TOKEN_NULL = 11;
static Dictionary<string, object> ParseObject( char[] json, ref int index, ref bool success ) {
static Dictionary<string, object> ParseObject( string json, ref int index, ref bool success ) {
Dictionary<string, object> table = new Dictionary<string, object>();
NextToken( json, ref index ); // skip {
@ -61,7 +62,7 @@ namespace SimpleJson {
}
}
static List<object> ParseArray( char[] json, ref int index, ref bool success ) {
static List<object> ParseArray( string json, ref int index, ref bool success ) {
List<object> array = new List<object>();
NextToken( json, ref index ); // [
@ -82,7 +83,7 @@ namespace SimpleJson {
}
}
static object ParseValue( char[] json, ref int index, ref bool success ) {
public static object ParseValue( string json, ref int index, ref bool success ) {
switch ( LookAhead( json, index ) ) {
case TOKEN_STRING:
return ParseString( json, ref index, ref success );
@ -107,7 +108,7 @@ namespace SimpleJson {
success = false; return null;
}
static string ParseString( char[] json, ref int index, ref bool success ) {
static string ParseString( string json, ref int index, ref bool success ) {
StringBuilder s = new StringBuilder( 400 );
EatWhitespace( json, ref index );
char c = json[index++]; // "
@ -129,7 +130,8 @@ namespace SimpleJson {
if( remainingLength >= 4 ) {
// parse the 32 bit hex into an integer codepoint
uint codePoint;
if( !( success = UInt32.TryParse( new String( json, index, 4 ), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out codePoint ) ) )
string str = json.Substring( index, 4 );
if( !( success = UInt32.TryParse( str, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out codePoint ) ) )
return "";
s.Append( new String( (char)codePoint, 1 ) );
@ -148,46 +150,37 @@ namespace SimpleJson {
}
const StringComparison caseless = StringComparison.OrdinalIgnoreCase;
static object ParseNumber( char[] json, ref int index, ref bool success ) {
static object ParseNumber( string json, ref int index, ref bool success ) {
EatWhitespace( json, ref index );
int lastIndex = GetLastIndexOfNumber( json, index );
int charLength = (lastIndex - index) + 1;
string str = new String( json, index, charLength );
string str = json.Substring( index, charLength );
index = lastIndex + 1;
if( str.IndexOf( ".", caseless ) != -1 || str.IndexOf( "e", caseless ) != -1 ) {
double number;
success = double.TryParse( str, NumberStyles.Any, CultureInfo.InvariantCulture, out number );
return number;
} else {
long number;
success = long.TryParse( str, NumberStyles.Any, CultureInfo.InvariantCulture, out number );
return number;
}
return str;
}
static int GetLastIndexOfNumber( char[] json, int index ) {
static int GetLastIndexOfNumber( string json, int index ) {
int lastIndex = index;
for( ; lastIndex < json.Length; lastIndex++ ) {
if( "0123456789+-.eE".IndexOf( json[lastIndex] ) == -1 )
if( "0123456789+-".IndexOf( json[lastIndex] ) == -1 )
break;
}
return lastIndex - 1;
}
static void EatWhitespace( char[] json, ref int index ) {
static void EatWhitespace( string json, ref int index ) {
for( ; index < json.Length; index++ ) {
if( " \t\n\r\b\f".IndexOf( json[index] ) == -1 )
break;
}
}
static int LookAhead( char[] json, int index ) {
static int LookAhead( string json, int index ) {
int saveIndex = index;
return NextToken( json, ref saveIndex );
}
static int NextToken( char[] json, ref int index ) {
static int NextToken( string json, ref int index ) {
EatWhitespace( json, ref index );
if( index == json.Length )
return TOKEN_NONE;
@ -215,7 +208,7 @@ namespace SimpleJson {
return TOKEN_NONE;
}
static bool CompareConstant( char[] json, ref int index, string value ) {
static bool CompareConstant( string json, ref int index, string value ) {
int remaining = json.Length - index;
if( remaining < value.Length ) return false;

View file

@ -1,16 +1,16 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading;
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading;
namespace Launcher2 {
public sealed class ClassicubeSession : IWebTask {
const string classicubeNetUri = "https://www.classicube.net/",
loginUri = "https://www.classicube.net/acc/login",
publicServersUri = "https://www.classicube.net/server/list",
playUri = "https://www.classicube.net/server/play/";
loginUri = "https://www.classicube.net/api/login/",
publicServersUri = "https://www.classicube.net/api/servers",
playUri = "https://www.classicube.net/api/server/";
const string wrongCredentialsMessage = "Login failed";
const string loggedInAs = @"<a href=""/acc"" class=""button"">";
StringComparison ordinal = StringComparison.Ordinal;
@ -55,109 +55,65 @@ namespace Launcher2 {
Username = user;
// Step 1: GET csrf token from login page.
var swGet = System.Diagnostics.Stopwatch.StartNew();
var getResponse = GetHtml( loginUri, classicubeNetUri );
string token = null;
foreach( string line in getResponse ) {
//Console.WriteLine( line );
if( line.StartsWith( @" <input id=""csrf_token""", ordinal ) ) {
const int tokenStart = 68;
int tokenEnd = line.IndexOf( '"', tokenStart );
token = line.Substring( tokenStart, tokenEnd - tokenStart );
Log( "cc token get took " + swGet.ElapsedMilliseconds );
swGet.Stop();
break;
}
}
string getResponse = GetHtmlAll( loginUri, classicubeNetUri );
int index = 0; bool success = true;
Dictionary<string, object> data =
(Dictionary<string, object>)Json.ParseValue( getResponse, ref index, ref success );
string token = (string)data["token"];
// Step 2: POST to login page with csrf token.
string loginData = String.Format(
"csrf_token={0}&username={1}&password={2}",
Uri.EscapeDataString( token ),
"username={0}&password={1}&token={2}",
Uri.EscapeDataString( user ),
Uri.EscapeDataString( password )
Uri.EscapeDataString( password ),
Uri.EscapeDataString( token )
);
Log( "cc login took " + swGet.ElapsedMilliseconds );
var sw = System.Diagnostics.Stopwatch.StartNew();
var response = PostHtml( loginUri, loginUri, loginData );
foreach( string line in response ) {
if( line.Contains( wrongCredentialsMessage ) ) {
throw new InvalidOperationException( "Wrong username or password." );
} else if( line.Contains( loggedInAs ) ) {
Log( "cc login took " + sw.ElapsedMilliseconds );
sw.Stop();
return;
}
}
string response = PostHtmlAll( loginUri, loginUri, loginData );
index = 0; success = true;
data = (Dictionary<string, object>)Json.ParseValue( response, ref index, ref success );
List<object> errors = (List<object>)data["errors"];
if( errors.Count > 0 )
throw new InvalidOperationException( "Wrong username or password." );
Username = (string)data["username"];
Log( "cc login took " + sw.ElapsedMilliseconds );
sw.Stop();
}
public ClientStartData GetConnectInfo( string hash ) {
string uri = playUri + hash;
var response = GetHtml( uri, classicubeNetUri );
ClientStartData data = new ClientStartData();
data.Username = Username;
string response = GetHtmlAll( uri, classicubeNetUri );
foreach( string line in response ) {
int index = 0;
// Look for <param name="x" value="x"> tags
if( (index = line.IndexOf( "<param", ordinal )) > 0 ) {
int nameStart = index + 13;
int nameEnd = line.IndexOf( '"', nameStart );
string paramName = line.Substring( nameStart, nameEnd - nameStart );
// Don't read param value by default so we avoid allocating unnecessary 'value' strings.
if( paramName == "server" ) {
data.Ip = GetParamValue( line, nameEnd );
} else if( paramName == "port" ) {
data.Port = GetParamValue( line, nameEnd );
} else if( paramName == "mppass" ) {
data.Mppass = GetParamValue( line, nameEnd );
}
}
}
return data;
}
static string GetParamValue( string line, int nameEnd ) {
int valueStart = nameEnd + 9;
int valueEnd = line.IndexOf( '"', valueStart );
return line.Substring( valueStart, valueEnd - valueStart );
int index = 0; bool success = true;
Dictionary<string, object> root =
(Dictionary<string, object>)Json.ParseValue( response, ref index, ref success );
List<object> list = (List<object>)root["servers"];
Dictionary<string, object> pairs = (Dictionary<string, object>)list[0];
return new ClientStartData( Username, (string)pairs["mppass"],
(string)pairs["ip"], (string)pairs["port"] );
}
public List<ServerListEntry> GetPublicServers() {
var sw = System.Diagnostics.Stopwatch.StartNew();
var response = GetHtml( publicServersUri, classicubeNetUri );
List<ServerListEntry> servers = new List<ServerListEntry>();
int index = -1;
string response = GetHtmlAll( publicServersUri, classicubeNetUri );
int index = 0; bool success = true;
Dictionary<string, object> root =
(Dictionary<string, object>)Json.ParseValue( response, ref index, ref success );
List<object> list = (List<object>)root["servers"];
string hash = null;
string name = null;
string players = null;
string maxPlayers = null;
foreach( string line in response ) {
if( line.StartsWith( " <strong><a href", ordinal ) ) {
const int hashStart = 34;
int hashEnd = line.IndexOf( '/', hashStart );
hash = line.Substring( hashStart, hashEnd - hashStart );
int nameStart = hashEnd + 3; // point to first char of name
int nameEnd = line.IndexOf( '<', nameStart );
name = line.Substring( nameStart, nameEnd - nameStart );
name = WebUtility.HtmlDecode( name );
index++;
}
if( index < 0 ) continue;
if( line.StartsWith( @" <td class=""players"">", ordinal ) ) {
const int playersStart = 24;
int playersEnd = line.IndexOf( '/', playersStart );
players = line.Substring( playersStart, playersEnd - playersStart );
int maxPlayersStart = playersEnd + 1;
int maxPlayersEnd = line.IndexOf( ']', playersStart );
maxPlayers = line.Substring( maxPlayersStart, maxPlayersEnd - maxPlayersStart );
servers.Add( new ServerListEntry( hash, name, players, maxPlayers, "" ) );
}
foreach( object server in list ) {
Dictionary<string, object> pairs = (Dictionary<string, object>)server;
servers.Add( new ServerListEntry(
(string)pairs["hash"], (string)pairs["name"],
(string)pairs["players"], (string)pairs["maxplayers"],
(string)pairs["uptime"], (string)pairs["mppass"],
(string)pairs["ip"], (string)pairs["port"] ) );
}
Log( "cc servers took " + sw.ElapsedMilliseconds );
sw.Stop();

View file

@ -73,11 +73,12 @@ namespace Launcher2 {
protected string GetHtmlAll( string uri, string referer ) {
HttpWebResponse response = MakeRequest( uri, referer, null );
using( Stream stream = response.GetResponseStream() ) {
using( StreamReader reader = new StreamReader( stream ) ) {
return reader.ReadToEnd();
}
}
return GetResponseAll( response );
}
protected string PostHtmlAll( string uri, string referer, string data ) {
HttpWebResponse response = MakeRequest( uri, referer, data );
return GetResponseAll( response );
}
protected IEnumerable<string> GetResponseLines( HttpWebResponse response ) {
@ -91,8 +92,16 @@ namespace Launcher2 {
}
}
protected string GetResponseAll( HttpWebResponse response ) {
using( Stream stream = response.GetResponseStream() ) {
using( StreamReader reader = new StreamReader( stream ) ) {
return reader.ReadToEnd();
}
}
}
protected static void Log( string text ) {
System.Diagnostics.Debug.WriteLine( text );
Console.WriteLine( text );
}
}
}

View file

@ -18,12 +18,25 @@ namespace Launcher2 {
/// <summary> How long the server has been 'alive'. </summary>
public string Uptime;
public ServerListEntry( string hash, string name, string players, string maxPlayers, string uptime ) {
/// <summary> IP address the server is hosted on. </summary>
public string IPAddress;
/// <summary> Port the server is hosted on. </summary>
public string Port;
/// <summary> Mppass specific for the user and this server. </summary>
public string Mppass;
public ServerListEntry( string hash, string name, string players, string maxPlayers,
string uptime, string mppass, string ip, string port ) {
Hash = hash;
Name = name;
Players = players;
MaximumPlayers = maxPlayers;
Uptime = uptime;
Mppass = mppass;
IPAddress = ip;
Port = port;
}
}
}

View file

@ -118,6 +118,30 @@ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Simple-json license
======================
Copyright (c) 2011, The Outercurve Foundation
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Ionic.Zlib license
==================
Microsoft Public License (Ms-PL)