Browse Source

Microsoft SQL Server support and Sanity Check page (#569)

* Create telemetry_mssql.sql

SQL to create the speedtest_users table on Microsoft SQL server.

This SQL is based on the original templates, and continues to uses nvarchar for all fields as the templates do – even though some of the fields should be int.

* Added support for Microsoft SQL Server

* New sanitycheck.php page plus required updates to telemetry_db.php

Created a sanity check page to verify that the required PHP extensions are installed, and that it is possible to connect to the database.

* Update README.md - MS SQL support

Added mention of Microsoft SQL Server as a supported DB engine.

* Added missing <tr> to sanitycheck.php
Tracy-B 2 years ago
parent
commit
5956e07a7a
5 changed files with 324 additions and 16 deletions
  1. 1 1
      README.md
  2. 189 0
      results/sanitycheck.php
  3. 94 14
      results/telemetry_db.php
  4. 30 0
      results/telemetry_mssql.sql
  5. 10 1
      results/telemetry_settings.php

+ 1 - 1
README.md

@@ -29,7 +29,7 @@ Works with mobile versions too.
 ## Server requirements
 * A reasonably fast web server with Apache 2 (nginx, IIS also supported)
 * PHP 5.4 (other backends also available)
-* MySQL database to store test results (optional, PostgreSQL and SQLite also supported)
+* MySQL database to store test results (optional, Microsoft SQL Server, PostgreSQL and SQLite also supported)
 * A fast! internet connection
 
 ## Installation videos

+ 189 - 0
results/sanitycheck.php

@@ -0,0 +1,189 @@
+<?php
+require_once 'telemetry_settings.php';
+require_once 'telemetry_db.php';
+require_once '../backend/getIP_util.php';
+
+error_reporting(E_ALL);
+
+$pass="<span class='Pass'>Pass</span>";
+$failed="<span class='Failed'>failed</span>";
+$na="<span class='na'>N/A</span>";
+?>
+
+<html>
+<head>
+<title>Speed Test installation sanity check</title>
+<style>
+	table,th,td { border: 1px solid;}
+	.Pass   { color:green;}
+	.Failed { color:red;}
+	.na     { color:orange;}
+	.SectionHeading { font-style: italic;}
+</style>
+</head>
+<body>
+<table><tr><th>Item</th><th>Status</th><th>Comment</th></tr>
+<tr><td colspan="3" class='SectionHeading'>PHP extensions</td></tr>
+
+<tr><td>gd</td><td>
+<?php
+if(extension_loaded('gd')){
+	echo $pass;
+} else {
+	echo $failed;
+}
+?>
+</td><td>Used to render result images.</td></tr>
+
+<tr><td>openssl</td><td>
+<?php
+if(extension_loaded('openssl')){
+	echo $pass;
+} else {
+	echo $failed;
+}
+?>
+</td><td></td></tr>
+
+<tr><td>pdo_sqlsrv</td><td>
+<?php
+if(!isset($db_type) || $db_type != 'mssql'){
+	echo $na;
+}elseif(extension_loaded('pdo_sqlsrv')){
+	echo $pass;
+} else {
+	echo $failed;
+}
+?>
+</td><td>Only required if using MS SQL.</td></tr>
+
+<tr><td>pdo_mysql</td><td>
+<?php
+if(!isset($db_type) || $db_type != 'mysql'){
+	echo $na;
+}elseif(extension_loaded('pdo_mysql')){
+	echo $pass;
+} else {
+	echo $failed;
+}
+?>
+</td><td>Only required if using mysql.</td></tr>
+
+<tr><td>pdo_sqlite</td><td>
+<?php
+if(!isset($db_type) || $db_type != 'sqlite'){
+	echo $na;
+}elseif(extension_loaded('pdo_sqlite')){
+	echo $pass;
+} else {
+	echo $failed;
+}
+?>
+</td><td>Only required if using sqlite.</td></tr>
+
+
+<tr><td>pdo_pgsql</td><td>
+<?php
+if(!isset($db_type) || $db_type != 'postgresql'){
+	echo $na;
+}elseif(extension_loaded('pdo_pgsql')){
+	echo $pass;
+} else {
+	echo $failed;
+}
+?>
+</td><td>Only required if using sqlite.</td></tr>
+
+
+<tr><td colspan="3" class='SectionHeading'>Database check</td></tr>
+<tr><td>Connecting to DB</td><td>
+<?php
+	$pdo = getPdo(true);
+	if (($pdo instanceof PDO)) {
+		echo $pass;
+		echo "</td><td></td>";
+	} else {
+		echo $failed;
+		echo "</td><td>". htmlspecialchars($pdo) . "</td>";
+	}
+?>
+</tr>
+
+<tr><td>Insert into DB</td><td>
+<?php
+	$ip = getClientIp();
+	$ispinfo="";
+	$extra='{"DBTest":"This is a simple test of the database.  No speed test was done."}';
+	$ua = $_SERVER['HTTP_USER_AGENT'];
+	$lang = '';
+	if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
+		$lang = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
+	}
+
+	$dl=$ul=$ping=$jitter="";
+	$log="";
+
+	$insertResult = insertSpeedtestUser($ip, $ispinfo, $extra, $ua, $lang, $dl, $ul, $ping, $jitter, $log, true);
+	
+	if(($insertResult instanceof Exception)){
+		echo $failed;
+		echo "</td><td>";
+		echo htmlspecialchars($insertResult->getMessage()) . "</td>";
+	} else {
+		echo $pass;
+		echo "</td><td></td>";
+	}
+?>
+</tr>
+
+<tr><td>Read from DB</td><td>
+<?php
+	if(!($insertResult instanceof Exception)){
+		$QueryResult = getSpeedtestUserById($insertResult,true);
+		
+		if(($QueryResult instanceof Exception)){
+			echo $failed;
+			echo "</td><td>";
+			echo htmlspecialchars($insertResult->getMessage()) . "</td>";
+		} elseif(!is_array($QueryResult)) {
+			echo $failed;
+			echo "</td><td>Test result not retrieved from database.</td>";
+		} else {
+			echo $pass;
+			echo "</td><td></td>";
+		}
+	} else {
+		echo "</td><td>Insert failed so can't test reading inserted data</td>";
+	}
+?>
+</tr>
+
+</table>
+</body>
+</html>
+<?php
+/*
+$speedtests = getLatestSpeedtestUsers();
+print_r ($speedtests);
+
+
+echo '   ';
+print_r($pdo);
+if(!isset($pdo)){
+	echo 'got nothing';
+} 
+if($pdo == false){
+	echo 'got a false';
+}
+if (!($pdo instanceof PDO)) {
+    echo 'not a PDO';
+}
+if (($pdo instanceof PDO)) {
+    echo 'is PDO';
+}
+
+
+$speedtest = getSpeedtestUserById(1);
+print_r ($speedtest);
+*/
+?>

+ 94 - 14
results/telemetry_db.php

@@ -7,18 +7,24 @@ define('TELEMETRY_SETTINGS_FILE', 'telemetry_settings.php');
 /**
  * @return PDO|false
  */
-function getPdo()
+function getPdo($returnErrorMessage = false)
 {
     if (
         !file_exists(TELEMETRY_SETTINGS_FILE)
         || !is_readable(TELEMETRY_SETTINGS_FILE)
     ) {
+		if($returnErrorMessage){
+			return 'missing TELEMETRY_SETTINGS_FILE';
+		} 
         return false;
     }
 
     require TELEMETRY_SETTINGS_FILE;
 
     if (!isset($db_type)) {
+		if($returnErrorMessage){
+			return "db_type not set in '" . TELEMETRY_SETTINGS_FILE . "'";
+		} 
         return false;
     }
 
@@ -27,6 +33,47 @@ function getPdo()
     ];
 
     try {
+        if ('mssql' === $db_type) {
+            if (!isset(
+                $MsSql_server,
+                $MsSql_databasename,
+				$MsSql_WindowsAuthentication
+            )) {
+				if($returnErrorMessage){
+					return "Required MSSQL database settings missing in '" . TELEMETRY_SETTINGS_FILE . "'";
+				} 
+                return false;
+            }
+			
+			if (!$MsSql_WindowsAuthentication and
+			    !isset(
+						$MsSql_username,
+						$MsSql_password,
+						)
+				) {
+				if($returnErrorMessage){
+					return "Required MSSQL database settings missing in '" . TELEMETRY_SETTINGS_FILE . "'";
+				} 
+                return false;
+            }
+            $dsn = 'sqlsrv:'
+                .'server='.$MsSql_server
+                .';Database='.$MsSql_databasename;
+			
+			if($MsSql_TrustServerCertificate === true){
+				$dsn = $dsn . ';TrustServerCertificate=1';
+			}
+			if($MsSql_TrustServerCertificate === false){
+				$dsn = $dsn . ';TrustServerCertificate=0';
+			}
+			
+			if($MsSql_WindowsAuthentication){
+				return new PDO($dsn, "", "", $pdoOptions);
+			} else {
+				return new PDO($dsn, $MySql_username, $MySql_password, $pdoOptions);
+			}
+        }
+
         if ('mysql' === $db_type) {
             if (!isset(
                 $MySql_hostname,
@@ -35,7 +82,10 @@ function getPdo()
                 $MySql_username,
                 $MySql_password
             )) {
-                return false;
+                if($returnErrorMessage){
+					return "Required mysql database settings missing in '" . TELEMETRY_SETTINGS_FILE . "'";
+				} 
+				return false;
             }
 
             $dsn = 'mysql:'
@@ -48,6 +98,9 @@ function getPdo()
 
         if ('sqlite' === $db_type) {
             if (!isset($Sqlite_db_file)) {
+				if($returnErrorMessage){
+					return "Required sqlite database settings missing in '" . TELEMETRY_SETTINGS_FILE . "'";
+				} 
                 return false;
             }
 
@@ -80,7 +133,10 @@ function getPdo()
                 $PostgreSql_username,
                 $PostgreSql_password
             )) {
-                return false;
+                if($returnErrorMessage){
+					return "Required postgresql database settings missing in '" . TELEMETRY_SETTINGS_FILE . "'";
+				} 
+				return false;
             }
 
             $dsn = 'pgsql:'
@@ -90,9 +146,15 @@ function getPdo()
             return new PDO($dsn, $PostgreSql_username, $PostgreSql_password, $pdoOptions);
         }
     } catch (Exception $e) {
+		if($returnErrorMessage){
+			return $e->getMessage();
+		} 
         return false;
     }
 
+	if($returnErrorMessage){
+		return "db_type '" . $db_type . "' not supported";
+	} 
     return false;
 }
 
@@ -109,12 +171,15 @@ function isObfuscationEnabled()
 }
 
 /**
- * @return string|false returns the id of the inserted column or false on error
+ * @return string|false returns the id of the inserted column or false on error if returnErrorMessage is false or a error message if returnErrorMessage is true
  */
-function insertSpeedtestUser($ip, $ispinfo, $extra, $ua, $lang, $dl, $ul, $ping, $jitter, $log)
+function insertSpeedtestUser($ip, $ispinfo, $extra, $ua, $lang, $dl, $ul, $ping, $jitter, $log, $returnExceptionOnError = false)
 {
     $pdo = getPdo();
     if (!($pdo instanceof PDO)) {
+		if($returnExceptionOnError){
+			return new Exception("Failed to get database connection object");
+		} 
         return false;
     }
 
@@ -129,6 +194,9 @@ function insertSpeedtestUser($ip, $ispinfo, $extra, $ua, $lang, $dl, $ul, $ping,
         ]);
         $id = $pdo->lastInsertId();
     } catch (Exception $e) {
+		if($returnExceptionOnError){
+			return $e;
+		} 
         return false;
     }
 
@@ -142,16 +210,19 @@ function insertSpeedtestUser($ip, $ispinfo, $extra, $ua, $lang, $dl, $ul, $ping,
 /**
  * @param int|string $id
  *
- * @return array|null|false returns the speedtest data as array, null
+ * @return array|null|false|exception returns the speedtest data as array, null
  *                          if no data is found for the given id or
- *                          false if there was an error
+ *                          false or an exception if there was an error (based on returnExceptionOnError)
  *
  * @throws RuntimeException
  */
-function getSpeedtestUserById($id)
+function getSpeedtestUserById($id,$returnExceptionOnError = false)
 {
     $pdo = getPdo();
     if (!($pdo instanceof PDO)) {
+		if($returnExceptionOnError){
+			return new Exception("Failed to get database connection object");
+		} 
         return false;
     }
 
@@ -170,6 +241,9 @@ function getSpeedtestUserById($id)
         $stmt->execute();
         $row = $stmt->fetch(PDO::FETCH_ASSOC);
     } catch (Exception $e) {
+		if($returnExceptionOnError){
+			return $e;
+		} 
         return false;
     }
 
@@ -195,14 +269,20 @@ function getLatestSpeedtestUsers()
         return false;
     }
 
+    require TELEMETRY_SETTINGS_FILE;
+	
     try {
-        $stmt = $pdo->query(
-            'SELECT
-            id, timestamp, ip, ispinfo, ua, lang, dl, ul, ping, jitter, log, extra
+		$sql = 'SELECT ';
+		
+		if('mssql' === $db_type) {$sql .= ' TOP(100) ';}
+		
+		$sql .= ' id, timestamp, ip, ispinfo, ua, lang, dl, ul, ping, jitter, log, extra
             FROM speedtest_users
-            ORDER BY timestamp DESC
-            LIMIT 100'
-        );
+            ORDER BY timestamp DESC ';
+			
+		if('mssql' !== $db_type) {$sql .= ' LIMIT 100 ';}
+		
+        $stmt = $pdo->query($sql);
 
         $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
 

+ 30 - 0
results/telemetry_mssql.sql

@@ -0,0 +1,30 @@
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+CREATE TABLE [dbo].[speedtest_users](
+	[id] [bigint] IDENTITY(120,1) NOT NULL,
+	[timestamp] [datetime] NOT NULL,
+	[ip] [nvarchar](max) NOT NULL,
+	[ispinfo] [nvarchar](max) NULL,
+	[extra] [nvarchar](max) NULL,
+	[ua] [nvarchar](max) NOT NULL,
+	[lang] [nvarchar](max) NOT NULL,
+	[dl] [nvarchar](max) NULL,
+	[ul] [nvarchar](max) NULL,
+	[ping] [nvarchar](max) NULL,
+	[jitter] [nvarchar](max) NULL,
+	[log] [nvarchar](max) NULL,
+ CONSTRAINT [PK_speedtest_users] PRIMARY KEY CLUSTERED 
+(
+	[id] ASC
+)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
+) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
+GO
+
+ALTER TABLE [dbo].[speedtest_users] ADD  CONSTRAINT [DF_speedtest_users_timestamp]  DEFAULT (getdate()) FOR [timestamp]
+GO
+
+

+ 10 - 1
results/telemetry_settings.php

@@ -1,6 +1,6 @@
 <?php
 
-// Type of db: "mysql", "sqlite" or "postgresql"
+// Type of db: "mssql", "mysql", "sqlite" or "postgresql"
 $db_type = 'mysql';
 // Password to login to stats.php. Change this!!!
 $stats_password = 'PASSWORD';
@@ -12,6 +12,15 @@ $redact_ip_addresses = false;
 // Sqlite3 settings
 $Sqlite_db_file = '../../speedtest_telemetry.sql';
 
+// mssql settings
+$MsSql_server = 'DB_HOSTNAME';
+$MsSql_databasename = 'DB_NAME';
+$MsSql_WindowsAuthentication = true;   #true or false
+$MsSql_username = 'USERNAME';          #not used if MsSql_WindowsAuthentication is true
+$MsSql_password = 'PASSWORD';          #not used if MsSql_WindowsAuthentication is true
+$MsSql_TrustServerCertificate = true;  #true, false or comment out for driver default
+#Download driver from https://docs.microsoft.com/en-us/sql/connect/php/download-drivers-php-sql-server?view=sql-server-ver16
+
 // Mysql settings
 $MySql_username = 'USERNAME';
 $MySql_password = 'PASSWORD';