跳到主要内容

Specification

1. Overview

To make HTTP requests to PGOS Backend, you need to put essential parameters in HTTP Header, request payload in HTTP Body and send it to the correct host.

webapi

In this document, we'll cover the basics of each component and how to put them together to make a correct HTTP request.

2. Components

2.1 Domain

2.1.1 For Title-Region-Wide

image-20231208103515487

The domain is specific for your title region. Obtain the domain name on the web portal here.

2.1.2 For Title-Wide

Domain is fixed: server.pgosglobal.com

2.2 ServerKey

image-20231208103548459

Server keys are configured on the title settings page of the web portal.

serverkey

The secret_id is the first part of serverkey: LTRN.

The secret_key is the last parts: NANI-D3TK-YQBM-MOUX.

Please note that the composition of server key: secret id and secret key. In the following steps, the secret id part will be put directly in the HTTP header, and the secret key part is used to calculate the server ticket.

2.3 TitleID & TitleRegionID

In PGOS, "title" stands for game. Each title is isolated and does not affect each other. Normally there are multiple "regions" for each title. That's the meaning of TitleID and TitleRegionID. Put them in HTTP header to identify for the specific region of your game.

2.4 Verify Method

2.4.1 For Title-Region-Wide

ServerTicket is essentially a piece of JSON data encrypted by the AES CBC algorithm.

Let's demonstrate the process using this example:

TitleRegionID: d_5_123 ServerKey: LTRN-NANI-D3TK-YQBM-MOUX

First, you need to generate the plain text of JSON data.

{ "title_region_id": "d_5_123", "secret_id": "LTRN", "time": 1600531200 }

Then make the AES Key, which is the secret_key but wiped all the - delimiters.

NANID3TKYQBMMOUX

Finally, invoke the AES CBC algorithm to encrypt the JSON data above.

Two parameters are required to run the AES CBC algorithm: AES Key and IV. The IV is a constant value for all PGOS requests: $3,.'/&^rgnjkl!#.

AESKey: NANID3TKYQBMMOUX
IV: $3,.'/&^rgnjkl!#

The base64 string of the final product of AES CBC algorithm is the ServerTicket.

2.4.2 For Title-Wide

Signature is essentially the sha256 of some fields and secret key.

Let's use the following example to demonstrate the process:

TitleID: 5 ServerKey: LTRN-NANI-D3TK-YQBM-MOUX

First, you need to generate the plain text of JSON data.

{
"title_id":"5",
"secret_id":"LTRN",
"secret_key":"NANI-D3TK-YQBM-MOUX",
"timestamp": 1719386647 // Current timestamp in second
}

Then, connect the key and value with = and then connect them with & in ascending lexicographical order of the key.

secret_id=LTRN&secret_key=NANI-D3TK-YQBM-MOUX&timestamp=1719386647&title_id=5

Finally, sha256 is calculated for the obtained string and converted to a string.

2.5 Putting it all Together

To sum up, then set the required HTTP headers, and finally put the request JSON data in the HTTP body.

2.5.1 For Title-Region-Wide

we need a POST request to the URL specific to your title region

POST https://d.server.pgos.intlgame.cn/title_svr/get_player_data HTTP/1.1
Host: d.server.pgos.intlgame.cn
Content-Type: application/json
Secretid: LTRN
Serverticket: O842PzLWMy2pXYwQwAY9ShHzPdj0kZL6TmIyDwd8wdRIxupNhS8Wx4I485lnDS+fU8KqVKX32OV7gstkQ/ZxZkcSIylWEYAlhKWVJVxhVLs=
Titleid: 5
Titleregionid: d_5_123

{
"player_id": "123456"
}

2.5.2 For Title-Wide

POST https://server.pgosglobal.com/title_svr/get_title_file_info HTTP/1.1
Host: server.pgosglobal.com
Content-Type: application/json
Secretid: LTRN
Titleid: 5
Secretid:LTRN,
Timestamp:1719386647
Signature: f9d9ad3cbd9859f7b80296eeb42019232a03c387a83769b5b9ea41ea506dafe6

{
"paths": ["/123456"]
}

3. Debug HTTP API via Postman

While you can access the HTTP API by writing code, you may need to debug the HTTP API first to ensure you understand the related protocols and parameters before starting to write the code.

You can follow these steps to use Postman to access the HTTP API.

3.1 For Title-Region-Wide

image-20230712171930712

Four components are needed to invoke HTTP API via Postman.

  • Determine the HTTP Method and URL, where the URL includes the service path to be accessed.
  • Fill in the HTTP Header, using the variable {{serverTicket}} for the ServerTicket field.
  • Fill in the HTTP Body, adhering to the JSON format specified in the interface protocol.
  • Fill in the Pre-request Script by copying the following code and change the required fields for your project.
// Change these fields for your project
const secretID = 'AAAA';
const secretKey = 'BBBB-CCCC-DDDD-EEEE';
const titleRegionID = 'YourTitleRegionID';
const titleID = 'YourTitleID';

const ServerTokenIv = CryptoJS.enc.Utf8.parse("$3,.'/&^rgnjkl!#");

class AesCbc {
constructor(key, iv) {
this.key = key;
this.iv = iv;
}

encrypt(text) {
const plainText = CryptoJS.enc.Utf8.parse(text);
const encrypted = CryptoJS.AES.encrypt(plainText, this.key, { iv: this.iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 });
return encrypted.toString();
}
}

function encryptServerTicket(key, ticketJsonStr) {
const ac = new AesCbc(CryptoJS.enc.Utf8.parse(key), ServerTokenIv);
return ac.encrypt(ticketJsonStr);
}

class SecretTicket {
constructor(titleRegionID, secretID, time) {
this.title_region_id = titleRegionID;
this.secret_id = secretID;
this.time = time;
}
}

function genServerTicket(secretID, secretKey, titleRegionID) {
const st = new SecretTicket(titleRegionID, secretID, Math.floor(Date.now() / 1000));
const ticketJson = JSON.stringify(st);
const key = secretKey.split('-').join('');
const ticket = encryptServerTicket(key, ticketJson);
return ticket;
}

const ticket = genServerTicket(secretID, secretKey, titleRegionID);

pm.environment.set('serverTicket', ticket);

3.2 For Title-Wide

image-20240702150752023

Four components are needed to invoke HTTP API via Postman.

  • Determine the HTTP Method and URL, where the URL includes the service path to be accessed.
  • Fill in the HTTP Header, using the variable {{signature}} for the Signature field.
  • Fill in the HTTP Body, adhering to the JSON format specified in the interface protocol.
  • Fill in the Pre-request Script by copying the following code and change the required fields for your project.
function mapToStr(param) {
const keys = Object.keys(param).sort();
const keyValuePairs = keys.map(key => `${key}=${param[key]}`);
return keyValuePairs.join('&');
}

function reqParamToStr(data) {
const param = {};
for (const k in data) {
if (typeof data[k] === 'object' && data[k] !== null && !Array.isArray(data[k])) {
param[k] = mapToStr(data[k]);
} else {
param[k] = data[k];
}
}
return mapToStr(param);
}


function sha256Hex(str) {
const hash = CryptoJS.SHA256(str);
const hexHash = hash.toString(CryptoJS.enc.Hex);
return hexHash;
}

function makeSig(param) {
const strToSig = reqParamToStr(param);
const sigStr = sha256Hex(strToSig);
return sigStr;
}

const result = makeSig({
title_id: pm.request.headers.get("TitleId"),
secret_id:pm.request.headers.get("SecretID"),
secret_key:"BBBB-CCCC-DDDD-EEEE",
timestamp:pm.request.headers.get("Timestamp"),
});

pm.environment.set("signature", result);

4. Access HTTP API via Code

The following samples demonstrate how to access HTTP API in different languages.

4.1 For Title-Region-Wide


import (
"bytes"
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"time"
)

const ServerTokenIv string = "$3,.'/&^rgnjkl!#"

type AesCbc struct {
key []byte
iv []byte
}

func NewAesCbc(key, iv []byte) *AesCbc {
return &AesCbc{key: key, iv: iv}
}

func (ac *AesCbc) Encrypt(text string) (string, error) {
plainText := []byte(text)
paddedPlainText := Pad(plainText)
block, err := aes.NewCipher(ac.key)
if err != nil {
return "", err
}
ciphertext := make([]byte, len(paddedPlainText))
mode := cipher.NewCBCEncrypter(block, ac.iv)
mode.CryptBlocks(ciphertext, paddedPlainText)
return base64.StdEncoding.EncodeToString(ciphertext), nil
}

func (ac *AesCbc) Decrypt(text string) (string, error) {
ciphertext, err := base64.StdEncoding.DecodeString(text)
if err != nil {
return "", err
}
block, err := aes.NewCipher(ac.key)
if err != nil {
return "", err
}
plaintext := make([]byte, len(ciphertext))
mode := cipher.NewCBCDecrypter(block, ac.iv)
mode.CryptBlocks(plaintext, ciphertext)
unpaddedPlaintext := UnPad(plaintext)
return string(unpaddedPlaintext), nil
}

func Pad(text []byte) []byte {
padding := aes.BlockSize - (len(text) % aes.BlockSize)
padText := bytes.Repeat([]byte{byte(padding)}, padding)
return append(text, padText...)
}

func UnPad(text []byte) []byte {
length := len(text)
unPadding := int(text[length-1])
return text[:(length - unPadding)]
}

func EncryptServerTicket(key, ticketJsonStr string) (string, error) {
ac := NewAesCbc([]byte(key), []byte(ServerTokenIv))
return ac.Encrypt(ticketJsonStr)
}

func DecryptServerTicket(key, ticketJsonStr string) (string, error) {
ac := NewAesCbc([]byte(key), []byte(ServerTokenIv))
return ac.Decrypt(ticketJsonStr)
}

type SecretTicket struct {
TitleRegionID string `json:"title_region_id"`
SecretID string `json:"secret_id"`
Time int64 `json:"time"`
}

func GenServerTicket(secretID, secretKey, titleRegionID string) (string, error) {
st := SecretTicket{
TitleRegionID: titleRegionID,
SecretID: secretID,
Time: time.Now().Unix(),
}
ticketJson, err := json.Marshal(st)
if err != nil {
return "", err
}
key := strings.Join(strings.Split(secretKey, "-"), "")
ticket, err := EncryptServerTicket(key, string(ticketJson))
return ticket, err
}

func SplitServerKey(serverKey string) (string, string) {
s := strings.Split(serverKey, "-")
if len(s) > 1 {
secretID := s[0]
secretKey := strings.Join(s[1:], "-")
return secretID, secretKey
}
return "", ""
}

func GetServerUrl(titleRegionID string) string {
location := "euff"
domainSuffix := "com"
arr := strings.Split(titleRegionID, "_")
if len(arr) > 0 {
location = arr[0]
}
if location == "t" || location == "d" || strings.HasPrefix(location, "cn") {
domainSuffix = "cn"
}
serverUrl := fmt.Sprintf("https://%s.server.pgos.intlgame.%s", location, domainSuffix)
return serverUrl
}

func InvokeBackendApi(titleID, titleRegionID, serverKey, apiUri string, body []byte) (*http.Header, []byte, error) {
secretID, secretKey := SplitServerKey(serverKey)
ticket, _ := GenServerTicket(secretID, secretKey, titleRegionID)
url := GetServerUrl(titleRegionID) + apiUri
client := &http.Client{
Timeout: time.Duration(3) * time.Second,
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(body))
if err != nil {
return nil, nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("TitleId", titleID)
req.Header.Set("TitleRegionId", titleRegionID)
req.Header.Set("SecretId", secretID)
req.Header.Set("ServerTicket", ticket)

resp, err := client.Do(req)
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()

rspBody, err := ioutil.ReadAll(resp.Body)
return &resp.Header, rspBody, nil
}

type PlayerInfoRequest struct {
PlayerId string `json:"player_id"`
}

func main() {
titleID := "your_title_id"
titleRegionID := "your_title_region_id"
serverKey := "your_server_key"

// e.g. invoke GetPlayerInfo
apiUri := "/player/get_player_info"

req := &PlayerInfoRequest{
PlayerId: "123",
}
body, _ := json.Marshal(req)

rspHeader, rspBody, err := InvokeBackendApi(titleID, titleRegionID, serverKey, apiUri, body)
if err != nil {
fmt.Printf("err: %s", err)
} else {
fmt.Printf("rspHeader:%v, rspBody: %s\n", rspHeader, string(rspBody))
}
}

4.2 For Title-Wide

package main

import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"sort"
"strconv"
"strings"
"time"
)

func SplitServerKey(serverKey string) (string, string) {
s := strings.Split(serverKey, "-")
if len(s) > 1 {
secretID := s[0]
secretKey := strings.Join(s[1:], "-")
return secretID, secretKey
}
return "", ""
}

func mapToStr(param map[string]interface{}) string {
var arrSlice []string
for key := range param {
arrSlice = append(arrSlice, key)
}

sort.Strings(arrSlice)
data := make([]string, 0, len(arrSlice))
for _, key := range arrSlice {
data = append(data, key+"="+fmt.Sprintf("%v", param[key]))
}
return strings.Join(data, "&")
}

func makeSig(param map[string]interface{}) string {
strToSig := mapToStr(param)
strByte := sha256.Sum256([]byte(strToSig))
return strings.ToLower(hex.EncodeToString(strByte[:]))
}

func InvokeTitleBackendApi(titleID string, serverKey string, path string, body []byte) (*http.Header, []byte, error) {
secretID, secretKey := SplitServerKey(serverKey)
url := fmt.Sprintf("https://server.pgosglobal.com%s", path)
client := &http.Client{
Timeout: time.Duration(3) * time.Second,
}

curTime := time.Now().Unix()
makeSigParam := map[string]interface{}{
"secret_key": secretKey,
"secret_id": secretID,
"title_id": titleID,
"timestamp": curTime,
}

signature := makeSig(makeSigParam)

req, err := http.NewRequest("POST", url, bytes.NewBuffer(body))
if err != nil {
return nil, nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("TitleId", titleID)
req.Header.Set("SecretId", secretID)
req.Header.Set("Timestamp", strconv.FormatInt(curTime, 10))
req.Header.Set("Signature", signature)

resp, err := client.Do(req)
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()

rspBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, nil, err
}

return &resp.Header, rspBody, nil
}

func main() {

titleID := "your_title_id"
serverKey := "your_server_key"

apiUri := "/title/get_title_file_info"

req := &GetTitleFileInfoRequest{
Paths: []string{"/logo.jpg"},
UrlValidTime: 3600,
}
body, _ := json.Marshal(req)

rspHeader, rspBody, err := InvokeTitleBackendApi(titleID, serverKey, apiUri, body)
if err != nil {
fmt.Printf("err: %s", err)
} else {
fmt.Printf("rspHeader:%v, rspBody: %s\n", rspHeader, string(rspBody))
}
}