1: <?php declare(strict_types=1);
2:
3: /**
4: * Clansuite Server Query
5: *
6: * SPDX-FileCopyrightText: 2003-2025 Jens A. Koch
7: * SPDX-License-Identifier: MIT
8: *
9: * For the full copyright and license information, please view
10: * the LICENSE file that was distributed with this source code.
11: */
12:
13: namespace Clansuite\ServerQuery\Util;
14:
15: use const PHP_INT_MAX;
16: use function random_int;
17:
18: /**
19: * Helper utilities for implementing a lightweight Steam Query proxy in PHP.
20: */
21: final class SteamProxyHelper
22: {
23: /**
24: * @var array<int>
25: */
26: private array $challenges = [];
27: private int $index = 0;
28: private readonly int $size;
29:
30: public static function jenkinsHash(int $value): int
31: {
32: // emulate the 32-bit Jenkins mix used in the C implementation
33: $value = ($value + 0x7ED55D16) + (($value << 12) & 0xFFFFFFFF);
34: $value = ($value ^ 0xC761C23C) ^ (($value >> 19) & 0xFFFFFFFF);
35: $value = ($value + 0x165667B1) + (($value << 5) & 0xFFFFFFFF);
36: $value = ($value + 0xD3A2646C) ^ (($value << 9) & 0xFFFFFFFF);
37: $value = ($value + 0xFD7046C5) + (($value << 3) & 0xFFFFFFFF);
38: $value = ($value ^ 0xB55A4F09) ^ (($value >> 16) & 0xFFFFFFFF);
39:
40: return $value & 0xFFFFFFFF;
41: }
42:
43: /**
44: * Constructor.
45: */
46: public function __construct(int $size = 6)
47: {
48: $this->size = $size > 0 ? $size : 6;
49:
50: // seed challenges
51: for ($i = 0; $i < $this->size; $i++) {
52: $this->challengeNew();
53: }
54: }
55:
56: /**
57: * challengeNew method.
58: */
59: public function challengeNew(): void
60: {
61: for ($i = 0; $i < 100; $i++) {
62: $next = ($this->index + $i) % $this->size;
63: $new = self::jenkinsHash(random_int(1, PHP_INT_MAX));
64:
65: if ($new === 0) {
66: continue;
67: }
68:
69: if ($new === 0xFFFFFFFF) {
70: continue;
71: }
72: $this->challenges[$next] = $new & 0xFFFFFFFF;
73: $this->index = $next;
74:
75: return;
76: }
77: }
78:
79: /**
80: * challengeGet method.
81: */
82: public function challengeGet(int $mutate): int
83: {
84: if (!isset($this->challenges[$this->index])) {
85: // If no challenge is set at the current index, generate a new one and use it
86: $this->challengeNew();
87: }
88:
89: $challenge = $this->challenges[$this->index] ?? 0;
90:
91: return ($challenge + self::jenkinsHash($mutate)) & 0xFFFFFFFF;
92: }
93:
94: /**
95: * challengeValidate method.
96: */
97: public function challengeValidate(int $challenge, int $mutate): bool
98: {
99: if ($challenge === 0 || $challenge === 0xFFFFFFFF) {
100: return false;
101: }
102:
103: $idx = $this->index;
104:
105: for ($i = 0; $i < $this->size; $i++) {
106: if (!isset($this->challenges[$idx])) {
107: return false;
108: }
109: $chVal = $this->challenges[$idx];
110: $check = ($chVal + self::jenkinsHash($mutate)) & 0xFFFFFFFF;
111:
112: if ($check === ($challenge & 0xFFFFFFFF)) {
113: return true;
114: }
115: $idx++;
116:
117: if ($idx === $this->size) {
118: $idx = 0;
119: }
120: }
121:
122: return false;
123: }
124: }
125: