#!/usr/bin/python
# -*- coding: Windows-1251 -*-

"""
=== Counter-Strike Linux Server patch v 0.2.1 ===

[RUSSIAN]

Вашему вниманию предлагается патч для Linux dedicated server для Half-Life.
Исправляет работу в режиме LAN без использования Steam (так называемый
no-WON patch) (нужен например, если у пользователей попросту нет Интернета),
а также исправляет работу последних версий Cheating Death. Также я выключил
дурацкое сообщение "NET_SendPacket ERROR:" которое у меня частенько выводится
из-за того, что центральный сервер Valve недоступен из нашей локальной сети.

Работает для библиотек engine_XXX.so, включая engine_amd64.so для версий
начиная как минимум с версии 16 (Февраль 2005) вплоть до Linux Server Engine
version 29 (Январь 2006), и с большой вероятностью будет работать для
следующих версий (пока Valve опять что-нибудь не придумает чтобы усложнить
жизнь честным пользователям).

Ниже идет английский вариант примерно того же текста, плюс обьяснение что и
где нужно исправлять, на случай если новые изменения движка опять приведут
к неработоспособности патча - чтобы знать, откуда начинать копать.


[ENGLISH]

Here's the no-WON patch for Half-Life dedicated Linux servers.
First, it makes the server work in purely local networks (e.g. when
clients have no means to log on to the STEAM server), second, it makes
CheatingDeath to work on such servers, third, it removes the annoying
message "NET_SendPacket ERROR:" which often happens to me because the
central Valve server is not accessible from my LAN.

Works for engine_XXX.so (including engine_amd64.so!) from latest
(as-of January 2006) Linux Server Engine version 29, also works for older
versions (at least starting from version 16, February 2005). It will most
likely also work for later versions, unless Valve invents some new cool way
to make life harder to their legal customers.


[TECHNICAL NOTES]

The no-WON fix touches the following routines:

000e7710 <NET_CompareClassBAdr>:
   e7710: 8b 44 24 04             mov    eax,DWORD PTR [esp+4]
   e7714: 3b 44 24 18             cmp    eax,DWORD PTR [esp+24]
   e7718: 75 1c                   jne    e7736 <NET_CompareClassBAdr+0x26>
   e771a: 83 f8 01                cmp    eax,0x1
   e771d: 74 10                   je     e772f <NET_CompareClassBAdr+0x1f>
   e771f: 83 f8 03                cmp    eax,0x3
   e7722: 75 12                   jne    e7736 <NET_CompareClassBAdr+0x26>
   e7724: 8b 44 24 1c             mov    eax,DWORD PTR [esp+28]
   e7728: 66 39 44 24 08          cmp    WORD PTR [esp+8],ax
   e772d: 75 07                   jne    e7736 <NET_CompareClassBAdr+0x26>
   e772f: b8 01 00 00 00          mov    eax,0x1
   e7734: eb 02                   jmp    e7738 <NET_CompareClassBAdr+0x28>
   e7736: 31 c0                   xor    eax,eax
   e7738: c3                      ret

If it returns 1, the player is admited no matter whether it's in a
class C network or not. So what we have to do is to fix this routine:

          b8 01 00 00 00          mov    eax,0x1
          c3                      ret

This script requires the `objdump' program. If you don't have it installed,
you must install the binutils package.

Also since Cheating Death 4.29.0 you have to apply an additional patch
in order to allow CD to work on patched servers. This includes changing
the strings VALVE_ID_LAN to STEAM_ID_LAN and VALVE_ID_PENDING to STEAM_ID_PENDING.

After steam update "Linux Server Engine version 29" Valve introduced an additional
check for Steam user key. In order to fix this one, you have to do two things:

First, you have to modify the SV_FinishCertificateCheck function to always return 1,
just like NET_CompareClassBAdr above. Second, you have to find in the function
SV_ConnectClient the following sequence (AMD64 version):

   bd631: 83 bc 24 c0 12 00 00 01 cmpl   $0x1,0x12c0(%rsp)
   bd639: 74 7d                   je     bd6b8 <SV_ConnectClient+0x770>

and change it so that it thinks that the memory location contains 1.
For x86 the same sequence looks like this:

   c7fc9: 83 7d ec 01             cmpl   $0x1,0xffffffec(%ebp)
   c7fcd: 75 14                   jne    c7fe3 <SV_ConnectClient+0x80f>

In this case we must modify the JNE so that it never happens.
"""

import os
import sys
import shutil


def patch_str (contents, findstr, replstr, times):
    for x in range(times):
        patch_ofs = contents.index (findstr)
        contents = contents [:patch_ofs] + replstr + contents [patch_ofs + len (replstr):]
    return contents


def patch_func (contents, func_ofs, func_len, findstr, replstr, times):
    pos = func_ofs
    for x in range(times):
        if findstr:
            pos = contents.index (findstr, pos, func_ofs + func_len)

        if contents [pos:pos + len(replstr)] == replstr:
            raise 'patch already applied'

        contents = contents [:pos] + replstr + contents [pos + len (replstr):]
        pos += 1

    return contents


def patch (fn):
    print "Patching file `%s' ..." % fn

    patches = [
        {
            'name': 'Make NET_CompareClassBAdr always return 1',
            'func': 'NET_CompareClassBAdr',
            'repl': "\xb8\x01\x00\x00\x00\xc3"
        },
        {
            'name': 'Make SV_FinishCertificateCheck always return 1',
            'func': 'SV_FinishCertificateCheck',
            'repl': "\xb8\x01\x00\x00\x00\xc3"
        },
        {
            'name': 'Force SV_ConnectClient to accept our certificate (x86_64)',
            'func': 'SV_ConnectClient',
            'find': '\x83\xbc\x24\xc0\x12\x00\x00\x01\x74',
            'repl': '\x83\xbc\x24\xc0\x12\x00\x00\x01\xeb'
        },
        {
            'name': 'Force SV_ConnectClient to accept our certificate (x86)',
            'func': 'SV_ConnectClient',
            'find': '\x83\x7d\xec\x01\x75',
            'repl': '\x83\x7d\xec\x01\x75\x00',
            'count': 2
        },
        {
            'name': 'Shut up the ugly "NET_SendPacket ERROR:" message',
            'find': 'NET_SendPacket ERROR:',
            'repl': '\0',
        },
        {
            'name': 'Use STEAM_ID instead of VALVE_ID for CheatingDeath',
            'find': 'VALVE_ID',
            'repl': 'STEAM',
            'count': 2
        },
    ];

    func_list = ''
    for patch in patches:
        if patch.get ('func'):
            if len (func_list) > 0:
                func_list += '|'
            func_list += patch.get ('func')

    x = os.popen ("objdump -t '" + fn + "' | egrep '(" + func_list + ")'");
    while 1:
        line = x.readline (1000)
        if not line:
            break
        tmp = line.split (None)
        try:
            for patch in patches:
                if patch.get ('func') == tmp [5]:
                    patch ['func_ofs'] = int (tmp [0], 16)
                    patch ['func_len'] = int (tmp [4], 16)
        except:
            pass
    x.close ();

    x = open (fn, "r+");

    # Now read the whole file
    x.seek (0, 0)
    contents = x.read (99999999)

    # Apply all patches
    patches_ok = 0
    for patch in patches:
        print '\t' + patch ['name'] + ':',
        try:
            if patch.get ('func'):
                contents = patch_func (contents, patch ['func_ofs'], patch ['func_len'],
                    patch.get ('find'), patch ['repl'], patch.get ('count', 1))
            else:
                contents = patch_str (contents,
                    patch.get ('find'), patch ['repl'], patch.get ('count', 1))

            patches_ok += 1
            print "DONE"

        except:
            print "FAILED"

    if patches_ok == 0:
        print "	File", fn, "has already been patched"
    else:
        # Backup the file before patching
        shutil.copyfile (fn, fn + ".orig")
        x.seek (0, 0)
        x.write (contents)

    x.close ();
    return


#----- Program entry point -----
if (len (sys.argv) < 2):
    print "Usage: " + sys.argv[0] + " [engine*.so]"
    sys.exit (-1)

for i in range(1, len(sys.argv)):
    patch (sys.argv [i])

