How I made a Minecraft payment acceptance system using pure PowerShell


In this article, we will screw the godless donut to the Minecraft vanilla server using Powershell. The advantage of the method is that minecraft is just a special case of the implementation of automatic payments using console commands. We just listen to what the payment system sends us and wrap it in a team. And most importantly - no plugins.
And we will accept payments through PayPal. Most importantly, in order to start accepting payments you do not need to change the code, PayPal will send us everything you need. We will use the buttons on the site, so that the site can do with pure HTML. We abstract from the intricacies of the payment system itself and concentrate only on the main points in the code.

By the way, the author will be very happy if you look through all of his modules and find children's errors in them that you point to or correct. Here is a link to the github project.

A few words about IPN


IPN


We will accept payments through the buttons. Buttons do not require any backend from you, they work in pure HTML, and they also have their own fields.

Buttons trigger IPN - Instant Payment Notification, in which data is sent to our WebListener. We will consider the IPN structure below.

In addition, anyone who has a PayPal account can make their own button.
IPN does not have the full PayPal REST API, but the basic functionality can be implemented on it. In fact, the IPN we are considering is not a REST API in the full sense of the word just because PayPal itself does not expect anything from us except code 200.

Raise WebListener


PayPal, for security reasons, does not send requests via HTTP, so we need to issue a certificate to get started. 

The author used WinAcme . You can issue a certificate to any domain, and you need to put the certificate in a local certificate store. By the way, WinAcme is located in the root of the disk in the image.

#     
Get-ChildItem -Path Cert:\LocalMachine\My 
 
#      443 .
netsh http add sslcert ipport=0.0.0.0:443 certhash=D106F5676534794B6767D1FB75B58D5E33906710 "appid={00112233-4455-6677-8899-AABBCCDDEEFF}"

Powershell can use classes from .net, which makes it almost equal to .net. First, using the HttpListener class, raise the Web server.

#   .net
$http = [System.Net.HttpListener]::new() 
 
#   
$http.Prefixes.Add("http://donate.to/")
$http.Prefixes.Add("https://donate.to/")
 
# 
$http.Start()

To verify that everything is done fine, run netstat.



If our script started listening to port 443 in the list, it means that you did everything correctly, and we can proceed to accepting request processing. Just do not forget about the firewall.

Accept request


Using IPN Simulator, we can send ourselves a test POST request to see what it is. But you cannot include your own fields in it, so the author recommends making a button and immediately buying something from yourself. IPN History will display a normal request from the button that you will use. The author did just that by purchasing one coal for one ruble.

We will accept using the While loop. While the web server is running, we can read the incoming data stream.

while ($http.IsListening) {
 
  $context = $http.GetContext()
 
  if ($context.Request.HttpMethod -eq 'POST' -and $context.Request.RawUrl -eq '/') {
 
    #  POST 
    $Reader = [System.IO.StreamReader]::new($context.Request.InputStream).ReadToEnd()
    
    #  .
    $DecodedContent = [System.Web.HttpUtility]::UrlDecode($Reader)
 
    #   .
    $Payment | Format-Table
 
    #  200 OK   .
    $context.Response.Headers.Add("Content-Type", "text/plain")
    $context.Response.StatusCode = 200
    $ResponseBuffer = [System.Text.Encoding]::UTF8.GetBytes("")
    $context.Response.ContentLength64 = $ResponseBuffer.Length
    $context.Response.OutputStream.Write($ResponseBuffer, 0, $ResponseBuffer.Length)
    $context.Response.Close()
  }
}

If you get a vermicelli like this, then apply:

$Payment = $DecodedContent -split "&" | ConvertFrom-StringData



After that, you will finally receive a normal object, where all Value is String.



You can quit reading right here if you don’t want to go deeper into the code, but just want to accept requests from someone’s API.

Here is the code that works right out of the box, copy and use:

# 
$http = [System.Net.HttpListener]::new() 
 
# ,   
$http.Prefixes.Add("http://localhost/")
$http.Prefixes.Add("https://localhost/")
 
$http.Start()
 
while ($http.IsListening) {
 
  $context = $http.GetContext()
 
  if ($context.Request.HttpMethod -eq 'POST' -and $context.Request.RawUrl -eq '/') {
 
    #  POST 
    $Reader = [System.IO.StreamReader]::new($context.Request.InputStream).ReadToEnd()
    
    #  .
    $DecodedContent = [System.Web.HttpUtility]::UrlDecode($Reader)
          
    #  IPN   
    $Payment = $DecodedContent -split "&" | ConvertFrom-StringData
 
    #   .
    $Payment | Format-Table
 
    #  200 OK   .
    $context.Response.Headers.Add("Content-Type", "text/plain")
    $context.Response.StatusCode = 200
    $ResponseBuffer = [System.Text.Encoding]::UTF8.GetBytes("")
    $context.Response.ContentLength64 = $ResponseBuffer.Length
    $context.Response.OutputStream.Write($ResponseBuffer, 0, $ResponseBuffer.Length)
    $context.Response.Close()
  }
}

Minecraft Nuances


So we figured out how we can receive alerts about payments, now we can credit them. But here, too, is not so simple. The problem is that the game does not give items or change the status of players who are not on the server. That is, we need to wait until a person enters the server to give him what he paid for.

Therefore, your attention is presented to the general concept of the smoker, for crediting payments.



Payments are received through the Listener above; only one line was added to it to write the object to the file. Complete-Payment (Processor) looks at the nickname and matches it with the file name. If it finds a file, compiles a command for rcon and executes it.

Start-minecraftabout which the author wrote in a previous article was slightly modified. Now he listens to the conclusion, looks at the nicknames of the players and passes them to the payment processor.

Making real callbacks


Without using plugins, we will make true callbacks. For this, Start-Minecraft has been modified. Now he not only knows how to add StdOut to a file, but also walks along each line with a regular schedule. Fortunately, minecraft leaves a very specific message when a player enters the server.

[04:20:00 INFO]: UUID of player XXPROHUNTERXX is 23e93d2e-r34d-7h15 -5h17-a9192cd70b48

It’s very easy to pick a nickname from this line. Here is all the code we need to get data from Stdout strings.

$Regex = [Regex]::new("of player ([^ ]+)")
 
powershell.exe -file ".\Start-MinecraftHandler.ps1" -type $type -MinecraftPath $MinecraftPath | Tee-Object $LogFile -Append | ForEach-Object {
 
     Write-host $_
        
    $Player = $Regex.Matches($_).value -replace "of player "
        
    if ($true -eq $Regex.Matches($_).Success) {
        #   
    }
}


A new line is fed into the $ _ pipeline, we write it in the console window and go through it regularly. The regular itself notifies us when it works, which is very convenient.

From here we can call any code. For example, using the same RCON, we can greet the player in the PM, using the bot in the discord to notify that someone has logged on to the server, ban the checkmate, and so on.

Making payments


Since we started processing payments, we would like to have at least quite complete data about the operation and the history of the operations performed, because we are talking about numbers with two zeros, so to speak.

The author wants to leave everything extremely simple and not to simulate a base yet. Let's look at the NoSQL approach. Let's create our own class, which will import all accepted payments into the / payments / folder in Json files.

    class Payment {
        #  .
        [datetime]$Date = [datetime]::ParseExact($i.payment_date, "HH:mm:ss MMM dd, yyyy PDT", [System.Globalization.CultureInfo]::InvariantCulture)
        # 
        [string]$Item = $i.item_name
        # 
        [UInt16]$Quantity = $i.Quantity
        #    
        [UInt16]$AmountPaid = $AmountPaid -as [UInt16]
        #     
        [string]$Currency = $i.mc_currency
        # ,   
        [string]$Player = $i.option_selection1
    
        [bool]$Completed = $false
        [UInt16]$ItemId = $i.item_number
    }
/source>

    , , ,          .

 ,    <b>option_selection1</b> –   .      input,   ,     .
     <b>option_selection1</b>,<b>option_selection2</b>   .

      ,     ,      .

<source lang="powershell"> #     Payment,        .
    $Payment = [Payment]::new()
    $Payment | Format-Table
    #  ,   ----
    $FileName = $Payment.Player + "-" + $Payment.date.Hour + "-" + $Payment.date.Minute + "-" + $Payment.date.Day + "-" + $Payment.date.Month + "-" + $Payment.date.Year + ".json"
 
# ,      
    $JsonPath = Join-Path $MinecraftPath \payments\Pending $FileName
    
    #   
    $Payment | ConvertTo-Json | Out-File $JsonPath

That's all that was required from our listener. Receive data from PayPal and write to a file.

We do payment processing


The handler will be called the regular one that was written about earlier. We transfer the player’s nickname to the module and that’s it. Next, a new script is launched, which searches for the file, and if there is a file, it gives the player the item that is written in the file.

powershell.exe -file "C:\mc.fern\Start-MinecraftHandler.ps1" -type $type -MinecraftPath $MinecraftPath | Tee-Object $LogFile -Append | ForEach-Object {
       
        #     ,      .
        Write-host $_   
 
        # Regex     
        if ($true -eq $Regex.Matches($_).Success) {
            
            #       
            $Player = $Regex.Matches($_).value -replace "of player "
            
            #  ,       
            Complete-Payment -Player $Player
        }
    }

When the regular is triggered, a module is launched that completes the payment, that is, it gives the player an item. To do this, in the / Payments / Pending / folder, the script searches for files containing the nickname of the player who entered the game and reads its contents.

Now you need to collect the command for the server and send it there. It will be collected from a file. We know the player’s nickname, the name of the item and its ID were recorded, how many pieces were also recorded, it remains only to send a command to the game server. For this we will use mcrcon .

#    
    $JsonPath = Join-Path $MinecraftPath\payments\Pending -ChildPath $Player*
    $i = $JsonPath | Get-Item | Where-Object { !$_.PSIsContainer } | Get-Content | ConvertFrom-Json -ErrorVariable Errored
 
    #      
    if ($null -ne $i) {
 
        #  
        $Command = '"' + "give " + $i.Player + " " + $i.Item + " " + $i.Quantity + '"'
        Write-host $Command -ForegroundColor Green
    
        #   
        Start-Process -FilePath mcrcon.exe -ArgumentList "-H localhost -p 123 -w 5 $Command"
    
        # ,      
        $JsonPath = Join-Path $MinecraftPath\payments\Pending -ChildPath $FileName
        
        #   
        $i | ConvertTo-Json | Out-File $JsonPath
    
        #     
        Move-Item  -Path $JsonPath -Destination $MinecraftPath\payments\Completed
    }

We make it all in a convenient module


The Java process and the WebListener process require different threads, but the author is not satisfied with the need to run the WebListener separately and the server separately. The author wants everything at once with one team.

Therefore, using Powershell 7, we will launch both this and that. And it will help us:

ForEach-Object -Parallel {}

The cmdlet works with inputObject, so we feed it an uncomplicated array, and share the streams using a switch.

"A", "B" | ForEach-Object -Parallel {
 
    Import-Module ".\Start-Minecraft.ps1"
 
    Import-Module ".\Start-WebListener.ps1"
 
    switch ($_) {
        "A" {
            Start-WebListener -Path "C:\mc\"
        }
        "B" {
            Start-Minecraft -Type Vanilla -LogFile ".\stdout.txt" -MinecraftPath "C:\mc\"
        }
        
    }
}

So in a crutch way, we started two different processes from one terminal and did not even lose input. But there was another problem. WebListener locks the console after a regular server shutdown and does not want to go anywhere.

In order not to restart the terminal each time, a random key was added to Start-MinecraftHandler.ps1 and to Start-WebListener.ps1, which will stop the server via POST on WebListener.

Start-MinecraftHandler.ps1, when it records a successful completion, executes the command:

Invoke-WebRequest -Method Post -Uri localhost -Body $StopToken | Out-Null

$ StopToken contains a random numeric value that is pre-passed by the startup script to both Listener and Handler. The Listener looks at what it received in the request and turns off if the request body matches $ StopToken.

if ($DecodedContent -eq $StopToken) {
        Write-Host "Stopping WebListener"
        $context.Response.Headers.Add("Content-Type", "text/plain")
        $context.Response.StatusCode = 200
        $ResponseBuffer = [System.Text.Encoding]::UTF8.GetBytes("")
        $context.Response.ContentLength64 = $ResponseBuffer.Length
        $context.Response.OutputStream.Write($ResponseBuffer, 0, $ResponseBuffer.Length)
        $context.Response.Close()
        $http.Close()
        break
      }

It’s safe enough, only RAM knows about the token and no one else. All modules are launched from under PowerShell 7, and the path to the modules for PowerShell 7 is different from the path in Windows Powershell. Everything was stacked here. Keep in mind when writing your own.

C:\Program Files\PowerShell\7\Modules

We make a config file


So that all this disgrace could be used without a severe headache, you need to make a normal config file. The file will contain variables and nothing more. The config clings using the standard:

Import-Module $MinecraftPath\config.ps1 -Force

We need to point out the most important thing. The domain that is being tapped is the regular one that is looking for the player’s nickname, for the output may vary from version to version, and the password is from rcon.

It looks like this:

#,    
$DomainName = "localhost"
 
# ,      
# ,  
$RegExp = "of player ([^ ]+)"
#    ,   ,  .
$RegExpCut = "of player "
 
#  rcon,     server.properties
$rconPassword = "123"

It is desirable to place the config in the server folder, because the script is looking for it in the root-MinecraftPath

How to use all this?


First of all, these scripts are installed and ready for use in the Ruvds marketplace , but if you have not yet a client or have not tried the image, here is a link to all the files in the repository , do not hesitate to commit. 

  1. Download and install PowerShell 7
  2. Download and unzip the modules archive


Now all the necessary modules and commands have appeared. What are they doing?

Start-minecraft


Options:

-Type
Forge or Vanilla. It starts the server either from Server.Jar or Forge, choosing the latest version that is in the folder.

-MinecraftPath
Points to the folder from which the server will be launched.

-LogFile
An alternative way to collect logs. Indicates a file in which everything that appears in the console will be written.

-StartPaymentListener
Along with the server, it starts and accepts payments. The payment acceptance itself is available as a separate module. Replaces the Start-Weblistener cmdlet

Start-weblistener


Starts the payment acceptance module.

-MinecraftPath
Points to the folder with the config file.

-StopToken
Specifies -Body HTTP POST request to stop WebListener'a.

Conclusion:


Well, miracles do happen.


All Articles