/*
* Copyright (c) Contributors, http://opensimulator.org/
* See CONTRIBUTORS.TXT for a full list of copyright holders.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the OpenSim Project nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
using System;
using System.Collections.Generic;
using System.Data;
using System.Reflection;
using OpenMetaverse;
using log4net;
using OpenSim.Framework;
namespace OpenSim.Data.MySQL
{
///
/// A MySQL Interface for the Grid Server
///
public class MySQLGridData : GridDataBase
{
private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
///
/// MySQL Database Manager
///
private MySQLManager database;
///
/// Better DB manager. Swap-in replacement too.
///
public Dictionary m_dbconnections = new Dictionary();
public int m_maxConnections = 10;
public int m_lastConnect;
public MySQLSuperManager GetLockedConnection()
{
int lockedCons = 0;
while (true)
{
m_lastConnect++;
// Overflow protection
if (m_lastConnect == int.MaxValue)
m_lastConnect = 0;
MySQLSuperManager x = m_dbconnections[m_lastConnect % m_maxConnections];
if (!x.Locked)
{
x.GetLock();
return x;
}
lockedCons++;
if (lockedCons > m_maxConnections)
{
lockedCons = 0;
System.Threading.Thread.Sleep(1000); // Wait some time before searching them again.
m_log.Debug(
"WARNING: All threads are in use. Probable cause: Something didnt release a mutex properly, or high volume of requests inbound.");
}
}
}
override public void Initialise()
{
m_log.Info("[MySQLGridData]: " + Name + " cannot be default-initialized!");
throw new PluginNotInitialisedException (Name);
}
///
/// Initialises Grid interface
///
///
/// - Loads and initialises the MySQL storage plugin
/// - Warns and uses the obsolete mysql_connection.ini if connect string is empty.
/// - Check for migration
///
///
///
/// connect string.
override public void Initialise(string connect)
{
if (connect != String.Empty)
{
database = new MySQLManager(connect);
m_log.Info("Creating " + m_maxConnections + " DB connections...");
for (int i = 0; i < m_maxConnections; i++)
{
m_log.Info("Connecting to DB... [" + i + "]");
MySQLSuperManager msm = new MySQLSuperManager();
msm.Manager = new MySQLManager(connect);
m_dbconnections.Add(i, msm);
}
}
else
{
m_log.Warn("Using deprecated mysql_connection.ini. Please update database_connect in GridServer_Config.xml and we'll use that instead");
IniFile GridDataMySqlFile = new IniFile("mysql_connection.ini");
string settingHostname = GridDataMySqlFile.ParseFileReadValue("hostname");
string settingDatabase = GridDataMySqlFile.ParseFileReadValue("database");
string settingUsername = GridDataMySqlFile.ParseFileReadValue("username");
string settingPassword = GridDataMySqlFile.ParseFileReadValue("password");
string settingPooling = GridDataMySqlFile.ParseFileReadValue("pooling");
string settingPort = GridDataMySqlFile.ParseFileReadValue("port");
database = new MySQLManager(settingHostname, settingDatabase, settingUsername, settingPassword,
settingPooling, settingPort);
m_log.Info("Creating " + m_maxConnections + " DB connections...");
for (int i = 0; i < m_maxConnections; i++)
{
m_log.Info("Connecting to DB... [" + i + "]");
MySQLSuperManager msm = new MySQLSuperManager();
msm.Manager = new MySQLManager(settingHostname, settingDatabase, settingUsername, settingPassword,
settingPooling, settingPort);
m_dbconnections.Add(i, msm);
}
}
// This actually does the roll forward assembly stuff
Assembly assem = GetType().Assembly;
Migration m = new Migration(database.Connection, assem, "GridStore");
// TODO: After rev 6000, remove this. People should have
// been rolled onto the new migration code by then.
TestTables(m);
m.Update();
}
#region Test and initialization code
///
/// Ensure that the user related tables exists and are at the latest version
///
private void TestTables(Migration m)
{
// we already have migrations, get out of here
if (m.Version > 0)
return;
Dictionary tableList = new Dictionary();
tableList["regions"] = null;
database.GetTableVersion(tableList);
UpgradeRegionsTable(tableList["regions"]);
// we have tables, but not a migration model yet
if (m.Version == 0)
m.Version = 1;
}
///
/// Create or upgrade the table if necessary
///
/// A null indicates that the table does not
/// currently exist
private void UpgradeRegionsTable(string oldVersion)
{
// null as the version, indicates that the table didn't exist
if (oldVersion == null)
{
database.ExecuteResourceSql("CreateRegionsTable.sql");
return;
}
if (oldVersion.Contains("Rev. 1"))
{
database.ExecuteResourceSql("UpgradeRegionsTableToVersion2.sql");
return;
}
if (oldVersion.Contains("Rev. 2"))
{
database.ExecuteResourceSql("UpgradeRegionsTableToVersion3.sql");
return;
}
}
#endregion
///
/// Shuts down the grid interface
///
override public void Dispose()
{
database.Close();
}
///
/// Returns the plugin name
///
/// Plugin name
override public string Name
{
get { return "MySql OpenGridData"; }
}
///
/// Returns the plugin version
///
/// Plugin version
override public string Version
{
get { return "0.1"; }
}
///
/// Returns all the specified region profiles within coordates -- coordinates are inclusive
///
/// Minimum X coordinate
/// Minimum Y coordinate
/// Maximum X coordinate
/// Maximum Y coordinate
/// Array of sim profiles
override public RegionProfileData[] GetProfilesInRange(uint xmin, uint ymin, uint xmax, uint ymax)
{
MySQLSuperManager dbm = GetLockedConnection();
try
{
Dictionary param = new Dictionary();
param["?xmin"] = xmin.ToString();
param["?ymin"] = ymin.ToString();
param["?xmax"] = xmax.ToString();
param["?ymax"] = ymax.ToString();
IDbCommand result =
dbm.Manager.Query(
"SELECT * FROM regions WHERE locX >= ?xmin AND locX <= ?xmax AND locY >= ?ymin AND locY <= ?ymax",
param);
IDataReader reader = result.ExecuteReader();
RegionProfileData row;
List rows = new List();
while ((row = dbm.Manager.readSimRow(reader)) != null)
{
rows.Add(row);
}
reader.Close();
result.Dispose();
return rows.ToArray();
}
catch (Exception e)
{
dbm.Manager.Reconnect();
m_log.Error(e.ToString());
return null;
}
finally
{
dbm.Release();
}
}
///
/// Returns up to maxNum profiles of regions that have a name starting with namePrefix
///
/// The name to match against
/// Maximum number of profiles to return
/// A list of sim profiles
override public List GetRegionsByName(string namePrefix, uint maxNum)
{
MySQLSuperManager dbm = GetLockedConnection();
try
{
Dictionary param = new Dictionary();
param["?name"] = namePrefix + "%";
IDbCommand result =
dbm.Manager.Query(
"SELECT * FROM regions WHERE regionName LIKE ?name",
param);
IDataReader reader = result.ExecuteReader();
RegionProfileData row;
List rows = new List();
while (rows.Count < maxNum && (row = dbm.Manager.readSimRow(reader)) != null)
{
rows.Add(row);
}
reader.Close();
result.Dispose();
return rows;
}
catch (Exception e)
{
dbm.Manager.Reconnect();
m_log.Error(e.ToString());
return null;
}
finally
{
dbm.Release();
}
}
///
/// Returns a sim profile from it's location
///
/// Region location handle
/// Sim profile
override public RegionProfileData GetProfileByHandle(ulong handle)
{
MySQLSuperManager dbm = GetLockedConnection();
try
{
Dictionary param = new Dictionary();
param["?handle"] = handle.ToString();
IDbCommand result = dbm.Manager.Query("SELECT * FROM regions WHERE regionHandle = ?handle", param);
IDataReader reader = result.ExecuteReader();
RegionProfileData row = dbm.Manager.readSimRow(reader);
reader.Close();
result.Dispose();
return row;
}
catch (Exception e)
{
dbm.Manager.Reconnect();
m_log.Error(e.ToString());
return null;
}
finally
{
dbm.Release();
}
}
///
/// Returns a sim profile from it's UUID
///
/// The region UUID
/// The sim profile
override public RegionProfileData GetProfileByUUID(UUID uuid)
{
MySQLSuperManager dbm = GetLockedConnection();
try
{
Dictionary param = new Dictionary();
param["?uuid"] = uuid.ToString();
IDbCommand result = dbm.Manager.Query("SELECT * FROM regions WHERE uuid = ?uuid", param);
IDataReader reader = result.ExecuteReader();
RegionProfileData row = dbm.Manager.readSimRow(reader);
reader.Close();
result.Dispose();
return row;
}
catch (Exception e)
{
dbm.Manager.Reconnect();
m_log.Error(e.ToString());
return null;
} finally
{
dbm.Release();
}
}
///
/// Returns a sim profile from it's Region name string
///
/// The sim profile
override public RegionProfileData GetProfileByString(string regionName)
{
if (regionName.Length > 2)
{
MySQLSuperManager dbm = GetLockedConnection();
try
{
Dictionary param = new Dictionary();
// Add % because this is a like query.
param["?regionName"] = regionName + "%";
// Order by statement will return shorter matches first. Only returns one record or no record.
IDbCommand result =
dbm.Manager.Query(
"SELECT * FROM regions WHERE regionName like ?regionName order by LENGTH(regionName) asc LIMIT 1",
param);
IDataReader reader = result.ExecuteReader();
RegionProfileData row = dbm.Manager.readSimRow(reader);
reader.Close();
result.Dispose();
return row;
}
catch (Exception e)
{
dbm.Manager.Reconnect();
m_log.Error(e.ToString());
return null;
}
finally
{
dbm.Release();
}
}
m_log.Error("[GRID DB]: Searched for a Region Name shorter then 3 characters");
return null;
}
///
/// Adds a new profile to the database
///
/// The profile to add
/// Successful?
override public DataResponse AddProfile(RegionProfileData profile)
{
MySQLSuperManager dbm = GetLockedConnection();
try {
if (dbm.Manager.insertRegion(profile))
{
return DataResponse.RESPONSE_OK;
}
return DataResponse.RESPONSE_ERROR;
}
finally
{
dbm.Release();
}
}
///
/// Update a sim profile
///
/// The profile to update
/// Sucessful?
/// Same as AddProfile
override public DataResponse UpdateProfile(RegionProfileData profile)
{
return AddProfile(profile);
}
///
/// Deletes a sim profile from the database
///
/// the sim UUID
/// Successful?
//public DataResponse DeleteProfile(RegionProfileData profile)
override public DataResponse DeleteProfile(string uuid)
{
MySQLSuperManager dbm = GetLockedConnection();
try {
if (dbm.Manager.deleteRegion(uuid))
{
return DataResponse.RESPONSE_OK;
}
return DataResponse.RESPONSE_ERROR;
} finally
{
dbm.Release();
}
}
///
/// DEPRECATED. Attempts to authenticate a region by comparing a shared secret.
///
/// The UUID of the challenger
/// The attempted regionHandle of the challenger
/// The secret
/// Whether the secret and regionhandle match the database entry for UUID
override public bool AuthenticateSim(UUID uuid, ulong handle, string authkey)
{
bool throwHissyFit = false; // Should be true by 1.0
if (throwHissyFit)
throw new Exception("CRYPTOWEAK AUTHENTICATE: Refusing to authenticate due to replay potential.");
RegionProfileData data = GetProfileByUUID(uuid);
return (handle == data.regionHandle && authkey == data.regionSecret);
}
///
/// NOT YET FUNCTIONAL. Provides a cryptographic authentication of a region
///
/// This requires a security audit.
///
///
///
///
///
public bool AuthenticateSim(UUID uuid, ulong handle, string authhash, string challenge)
{
// SHA512Managed HashProvider = new SHA512Managed();
// Encoding TextProvider = new UTF8Encoding();
// byte[] stream = TextProvider.GetBytes(uuid.ToString() + ":" + handle.ToString() + ":" + challenge);
// byte[] hash = HashProvider.ComputeHash(stream);
return false;
}
///
/// Adds a location reservation
///
/// x coordinate
/// y coordinate
///
override public ReservationData GetReservationAtPoint(uint x, uint y)
{
MySQLSuperManager dbm = GetLockedConnection();
try
{
Dictionary param = new Dictionary();
param["?x"] = x.ToString();
param["?y"] = y.ToString();
IDbCommand result =
dbm.Manager.Query(
"SELECT * FROM reservations WHERE resXMin <= ?x AND resXMax >= ?x AND resYMin <= ?y AND resYMax >= ?y",
param);
IDataReader reader = result.ExecuteReader();
ReservationData row = dbm.Manager.readReservationRow(reader);
reader.Close();
result.Dispose();
return row;
}
catch (Exception e)
{
dbm.Manager.Reconnect();
m_log.Error(e.ToString());
return null;
} finally
{
dbm.Release();
}
}
}
}