1: <?php
2:
3: /**
4: * This file is part of GameQ.
5: *
6: * GameQ is free software; you can redistribute it and/or modify
7: * it under the terms of the GNU Lesser General Public License as published by
8: * the Free Software Foundation; either version 3 of the License, or
9: * (at your option) any later version.
10: *
11: * GameQ is distributed in the hope that it will be useful,
12: * but WITHOUT ANY WARRANTY; without even the implied warranty of
13: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14: * GNU Lesser General Public License for more details.
15: *
16: * You should have received a copy of the GNU Lesser General Public License
17: * along with this program. If not, see <http://www.gnu.org/licenses/>.
18: */
19:
20: namespace GameQ\Protocols;
21:
22: use GameQ\Exception\Protocol as Exception;
23: use GameQ\Server;
24:
25: /**
26: * Epic Online Services Protocol Class
27: *
28: * Serves as a base class for EOS-powered games.
29: *
30: * @package GameQ\Protocols
31: * @author H.Rouatbi
32: */
33: class Eos extends Http
34: {
35: /**
36: * The protocol being used
37: *
38: * @var string
39: */
40: protected $protocol = 'eos';
41:
42: /**
43: * Longer string name of this protocol class
44: *
45: * @var string
46: */
47: protected $name_long = 'Epic Online Services';
48:
49: /**
50: * String name of this protocol class
51: *
52: * @var string
53: */
54: protected $name = 'eos';
55:
56: /**
57: * Grant type used for authentication
58: *
59: * @var string
60: */
61: protected $grant_type = 'client_credentials';
62:
63: /**
64: * Deployment ID for the game or application
65: *
66: * @var string
67: */
68: protected $deployment_id = null;
69:
70: /**
71: * User ID for authentication
72: *
73: * @var string
74: */
75: protected $user_id = null;
76:
77: /**
78: * User secret key for authentication
79: *
80: * @var string
81: */
82: protected $user_secret = null;
83:
84: /**
85: * Holds the server ip so we can overwrite it back
86: *
87: * @var string
88: */
89: protected $serverIp = null;
90:
91: /**
92: * Holds the server port query so we can overwrite it back
93: *
94: * @var string
95: */
96: protected $serverPortQuery = null;
97:
98: /**
99: * Normalize some items
100: *
101: * @var array
102: */
103: protected $normalize = [
104: // General
105: 'general' => [
106: // target => source
107: 'hostname' => 'hostname',
108: 'mapname' => 'mapname',
109: 'maxplayers' => 'maxplayers',
110: 'numplayers' => 'numplayers',
111: 'password' => 'password',
112: ]
113: ];
114:
115: /**
116: * Process the response from the EOS API
117: *
118: * @return array
119: * @throws Exception
120: */
121: public function processResponse()
122: {
123: $index = ($this->grant_type === 'external_auth') ? 2 : 1;
124: $server_data = isset($this->packets_response[$index]) ? json_decode($this->packets_response[$index], true) : null;
125:
126: $server_data = isset($server_data['sessions']) ? $server_data['sessions'] : null;
127:
128: // If no server data, throw an exception
129: if (empty($server_data)) {
130: throw new Exception('No server data found. Server might be offline.');
131: }
132:
133: return $server_data;
134: }
135:
136: /**
137: * Called before sending the request
138: *
139: * @param Server $server
140: */
141: public function beforeSend(Server $server)
142: {
143: $this->serverIp = $server->ip();
144: $this->serverPortQuery = $server->portQuery();
145:
146: // Authenticate and get the access token
147: $auth_token = $this->authenticate();
148:
149: if (!$auth_token) {
150: return;
151: }
152:
153: // Query for server data
154: $this->queryServers($auth_token);
155: }
156:
157: /**
158: * Authenticate to get the access token
159: *
160: * @return string|null
161: */
162: protected function authenticate()
163: {
164: $auth_url = "https://api.epicgames.dev/auth/v1/oauth/token";
165: $auth_headers = [
166: 'Authorization: Basic ' . base64_encode("{$this->user_id}:{$this->user_secret}"),
167: 'Accept-Encoding: deflate, gzip',
168: 'Content-Type: application/x-www-form-urlencoded',
169: ];
170:
171: $auth_postfields = "grant_type={$this->grant_type}&deployment_id={$this->deployment_id}";
172:
173: if ($this->grant_type === 'external_auth') {
174: // Perform device authentication if necessary
175: $device_auth = $this->deviceAuthentication();
176: if (!$device_auth) {
177: return null;
178: }
179: $auth_postfields .= "&external_auth_type=deviceid_access_token"
180: . "&external_auth_token={$device_auth['access_token']}"
181: . "&nonce=ABCHFA3qgUCJ1XTPAoGDEF&display_name=User";
182: }
183:
184: // Make the request to get the access token
185: $response = $this->httpRequest($auth_url, $auth_headers, $auth_postfields);
186:
187: return isset($response['access_token']) ? $response['access_token'] : null;
188: }
189:
190: /**
191: * Query the EOS server for matchmaking data
192: *
193: * @param string $auth_token
194: * @return array|null
195: */
196: protected function queryServers($auth_token)
197: {
198: $server_query_url = "https://api.epicgames.dev/matchmaking/v1/{$this->deployment_id}/filter";
199: $query_headers = [
200: "Authorization: Bearer {$auth_token}",
201: 'Accept: application/json',
202: 'Content-Type: application/json',
203: ];
204:
205: $query_body = json_encode([
206: 'criteria' => [
207: [
208: 'key' => 'attributes.ADDRESS_s',
209: 'op' => 'EQUAL',
210: 'value' => $this->serverIp,
211: ],
212: ],
213: 'maxResults' => 200,
214: ]);
215:
216: $response = $this->httpRequest($server_query_url, $query_headers, $query_body);
217:
218: return isset($response['sessions']) ? $response['sessions'] : null;
219: }
220:
221: /**
222: * Handle device authentication for external auth type
223: *
224: * @return array|null
225: */
226: protected function deviceAuthentication()
227: {
228: $device_auth_url = "https://api.epicgames.dev/auth/v1/accounts/deviceid";
229: $device_auth_headers = [
230: 'Authorization: Basic ' . base64_encode("{$this->user_id}:{$this->user_secret}"),
231: 'Accept-Encoding: deflate, gzip',
232: 'Content-Type: application/x-www-form-urlencoded',
233: ];
234:
235: $device_auth_postfields = "deviceModel=PC";
236:
237: return $this->httpRequest($device_auth_url, $device_auth_headers, $device_auth_postfields);
238: }
239:
240: /**
241: * Execute an HTTP request
242: *
243: * @param string $url
244: * @param array $headers
245: * @param string $postfields
246: * @return array|null
247: */
248: protected function httpRequest($url, $headers, $postfields)
249: {
250: $ch = curl_init();
251:
252: curl_setopt($ch, CURLOPT_URL, $url);
253: curl_setopt($ch, CURLOPT_POST, 1);
254: curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
255: curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
256: curl_setopt($ch, CURLOPT_POSTFIELDS, $postfields);
257:
258: $response = curl_exec($ch);
259:
260: if (!$response) {
261: return null;
262: }
263:
264: $this->packets_response[] = $response;
265:
266: return json_decode($response, true);
267: }
268:
269: /**
270: * Safely retrieves an attribute from an array or returns a default value.
271: *
272: * @param array $attributes
273: * @param string $key
274: * @param mixed $default
275: * @return mixed
276: */
277: protected function getAttribute($attributes, $key, $default = null)
278: {
279: return isset($attributes[$key]) ? $attributes[$key] : $default;
280: }
281: }
282: