Skip to content

Instantly share code, notes, and snippets.

@linusyang
Created November 17, 2013 17:41
Show Gist options
  • Save linusyang/7515929 to your computer and use it in GitHub Desktop.
Save linusyang/7515929 to your computer and use it in GitHub Desktop.
New WebKit-based Opera Mobile Off-Road Proxy Modifier
#!/usr/bin/env python
#-*- encoding: utf-8 -*-
#
# Opera Mobile Off-Road Proxy Modifier
# By Linus Yang <laokongzi@gmail.com>
# Licensed under GPLv3
#
from optparse import OptionParser
from zipfile import ZipFile, ZipInfo, ZIP_DEFLATED
from M2Crypto import BIO, SMIME, X509
import os
import zlib
import hashlib
import base64
class OperaPackageTuner(object):
VERSION = '0.1'
OVERRIDE_DOMAIN_LIST = ['server4.operamini.com', 'mini5.opera-mini.net', 'mini5cn.opera-mini.net', 'thumbnails.opera.com']
DEFAULT_HTTP_PORT = 80
DEFAULT_SOCKET_PORT = 1080
KEY_CN = 'jGDSpoEfhTZq8jGuQWgxsJQJthTpz6j96NhXfokmNuDgt6FR+WAbkwv1J+qKIr/m+19yUGvT6Bs7VdGJrxfjWy1+ph2Euk5izxwBeJ7bLD88APw8Ce4fyWJzZylHJ+Uq9MmQUW2Neq1OANarUM2MpjcF3wryQ+Zm2tKC1lFLZWeA4QjVkc94kg973uIe0UGaCAZVyirNrcTmTboBtazPcw=='
KEY_GLOBAL = 'wd16t34slndG/hBoECbJIPhkgRMhvLi+a7+loD/aThbJyNs68oD3cDNm53jpPFXnFZqIUtKxOB5SGjN/IrFAbN30GjEUrstPS/554MWqK6iCT8mJy4vcv47FzvUXa/1AWfIpuRv6AlEmspX5xAnnX29kFe4JT9f139OVofQxZoxaCOiN6JHcTdONTpqpucANxgSgQo46paKMz6da8JkUew=='
PKEY = '''-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDWkxkE3sYLJLHt
x2Lg2dglPj7NbOsd4v8GjKjovKjNa9N4bqcKp2zmDrsPmTVZ/9k+d6lD5+g9S2S4
5P6i0+ZW8eJnqBu/sjC1eMIEQ75Mchi4RvUhFYbwOKFOicK+OH+Ovs+PysPaHuMw
yeqT0KfD3ErzUCINUAgHMuCAlxfuagUzWeamlOwss/KEoKRmyHqU2DsxCTpnNy4v
ZBLAbm1C8VgY3/4DgcwM1ETabN3DuCRYGUgBsyVkE0+/3pjJKHdI2/VnalQNgVTI
u8oHueJHVTMRxGua92/e7MyOaefIotCOeCYglD+Zcn08BP5ymR2Z35uuOKCyF3+j
HVtq/ukfAgEDAoIBAQCPDLtYlIQHbcvz2kHrO+VuKX8znfIT7KoEXcXwfcXeR+JQ
ScSxxPNECdIKZiORVTt++nDX7/Ao3O3QmKnB4pmPS+xFGr0qdssjpdatgn7doWXQ
L04WDln1exY0W9cpev+0fzUKhy08FJd12/G34G/X6DH3isFeNVqvd0BVug/0RXWi
hnmONcUztAJ25E5YNqHadWSt+vU4pJOpvxDyE6ZXrBIpHBvlaZf8atJ7maf8iXfS
ZUzrqnx1O5zaTGRnGo7o/UdrfuLDfpVXnXBEHm+rk6QTq2ZKyZj6JZQ/K1LB+cXq
ZO9KG8oBSecXohQBeJYIDEikB9xHdsvelr1MoYR7AoGBAOrAmRccm5UnjAe/npdF
GIVXkXaep7Ur9rqT4NaoSMSnDRim6Kii2lNoZ2szvvKYuxRNmvi1u60iRvQsLM10
duqyG+FKdx+S5632ALWTKvdH97l3VYcRCrDYAyMYdotYavF8bcT9QKgYHoWHb18K
LL27A4afIXmrVXCnWXp1e2GbAoGBAOn+9xk0qK83mecSq5edXgJ1lq2NaRVmSZYc
5KKtCC8YYiQ0TSuIiRSpzJ3tR28wLtxO5lvqd72R8vBMPzS6CbY5RCj7tOBVW8bP
TuwOYUN+AAN87csZvlmPsUsXMmBNQTYycvo0Keh/ZR0RIoFmN37SyagZC1ybj90t
4cUCkUDNAoGBAJyAZg9oZ7jFCAUqabouEFjlC6RpxSNypHxileRwMIMaCLsZ8HBs
kYzwRPIif0xl0g2JEfsj0nNsL01yyIj4T0chZ+uG+hUMmnP5Vc5iHKTapSZPjloL
XHXlV2y6+bI68fZS89io1cVlaa5aSj9cHdPSAlm/a6ZyOPXE5lGjp5ZnAoGBAJv/
T2YjGx96ZpoMcmUTlAGjuckI8Lju27lomGxzWsoQQW14M3JbBg3GiGlI2kogHz2J
7ufxpSkL90rdf3h8Bnl7gsX9I0A459nfifK0QNepVVeonodmfuZfy4dkzEAzgM7M
TKbNcUWqQ2i2FwDuz6nh28VmB5MSX+jJQS4BtiszAoGAYyqt2RrdpGLZlaZyYlsF
zalGIfTpWXPuj5ot63Ghwawb0xoN1qKJdYcbanvrblVhtKEsYKOkae96d1grNcf4
Vbm3bMrPwHdIRf6pRS+x46mMBfuap1JoGcXESY4NwdsbpYo71PuBgykeNHaO2nq0
BYcm/RyNFHuJZd+PFfOevDc=
-----END PRIVATE KEY-----
'''
CERT = '''-----BEGIN CERTIFICATE-----
MIIEqDCCA5CgAwIBAgIJAJNurL4H8gHfMA0GCSqGSIb3DQEBBQUAMIGUMQswCQYD
VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4g
VmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UE
AxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAe
Fw0wODAyMjkwMTMzNDZaFw0zNTA3MTcwMTMzNDZaMIGUMQswCQYDVQQGEwJVUzET
MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4G
A1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9p
ZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZI
hvcNAQEBBQADggENADCCAQgCggEBANaTGQTexgskse3HYuDZ2CU+Ps1s6x3i/waM
qOi8qM1r03hupwqnbOYOuw+ZNVn/2T53qUPn6D1LZLjk/qLT5lbx4meoG7+yMLV4
wgRDvkxyGLhG9SEVhvA4oU6Jwr44f46+z4/Kw9oe4zDJ6pPQp8PcSvNQIg1QCAcy
4ICXF+5qBTNZ5qaU7Cyz8oSgpGbIepTYOzEJOmc3Li9kEsBubULxWBjf/gOBzAzU
RNps3cO4JFgZSAGzJWQTT7/emMkod0jb9WdqVA2BVMi7yge54kdVMxHEa5r3b97s
zI5p58ii0I54JiCUP5lyfTwE/nKZHZnfm644oLIXf6MdW2r+6R8CAQOjgfwwgfkw
HQYDVR0OBBYEFEhZAFY9JyxGrhGGBaR0GawJyowRMIHJBgNVHSMEgcEwgb6AFEhZ
AFY9JyxGrhGGBaR0GawJyowRoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UE
CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMH
QW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAG
CSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJAJNurL4H8gHfMAwGA1Ud
EwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAHqvlozrUMRBBVEY0NqrrwFbinZa
J6cVosK0TyIUFf/azgMJWr+kLfcHCHJsIGnlw27drgQAvilFLAhLwn62oX6snb4Y
LCBOsVMR9FXYJLZW2+TcIkCRLXWG/oiVHQGo/rWuWkJgU134NDEFJCJGjDbiLCpe
+ZTWHdcwauTJ9pUbo8EvHRkU3cYfGmLaLfgn9gP+pWA7LFQNvXwBnDa6sppCccEX
31I828XzgXpJ4O+mDL1/dBd+ek8ZPUP0IgdyZm5MTYPhvVqGCHzzTy3sIeJFymwr
sBbmg2OAUNLEMO6nwmocSdN2ClirfxqCzJOLSDE4QyS9BAH6EhY6UFcOaE0=
-----END CERTIFICATE-----
'''
def __init__(self, http_domain, socket_domain=None, http_port=80, socket_port=1080):
if socket_domain == None:
socket_domain = http_domain
self.http_domain = http_domain
self.socket_domain = socket_domain
self.http_port = http_port
self.socket_port = socket_port
self.file_date = None
def log(self, message):
print('%s' % (message))
def bin_replace(self, source, orig, dest):
pad_num = len(orig) - len(dest)
if pad_num < 0:
self.log('Warning: Ignore replacing with shorter string: "%s" -> "%s"' % (orig, dest))
return source
pad_dest = dest + '\x00' * pad_num
if len(orig) != len(pad_dest):
self.log('Warning: Unexpected inequality of length')
return source
return source.replace(bytes(orig), bytes(pad_dest))
def opm_patch(self, source):
patch_dict = {self.KEY_CN: self.KEY_GLOBAL}
for orig_domain in self.OVERRIDE_DOMAIN_LIST:
http_orig = 'http://%s:%d' % (orig_domain, self.DEFAULT_HTTP_PORT)
socket_orig = 'socket://%s:%d' % (orig_domain, self.DEFAULT_SOCKET_PORT)
http_dest = 'http://%s:%d' % (self.http_domain, self.http_port)
socket_dest = 'socket://%s:%d' % (self.socket_domain, self.socket_port)
patch_dict[http_orig] = http_dest
patch_dict[socket_orig] = socket_dest
for orig in patch_dict:
dest = patch_dict.get(orig)
source = self.bin_replace(source, orig, dest)
return source
def sha1_hash_base64(self, data):
return base64.b64encode(hashlib.sha1(data).digest())
def get_hash_entry(self, file_name, file_data):
mf_entry = "Name: %s\r\nSHA1-Digest: %s\r\n\r\n" % (file_name, self.sha1_hash_base64(file_data))
sf_entry = "Name: %s\r\nSHA1-Digest: %s\r\n\r\n" % (file_name, self.sha1_hash_base64(mf_entry))
return mf_entry, sf_entry
def smime_sign(self, data):
signer = SMIME.SMIME()
bio_pkey = BIO.MemoryBuffer(self.PKEY)
bio_cert = BIO.MemoryBuffer(self.CERT)
signer.load_key_bio(bio_pkey, bio_cert)
bio_raw = BIO.MemoryBuffer(data)
p7 = signer.sign(bio_raw, flags=SMIME.PKCS7_NOATTR | SMIME.PKCS7_DETACHED)
bio_signed = BIO.MemoryBuffer()
p7.write_der(bio_signed)
return bio_signed.read()
def get_cert_date(self):
if not self.file_date:
cert = X509.load_cert_string(self.CERT)
cert_time = cert.get_not_before().get_datetime()
self.file_date = (cert_time.year, cert_time.month, cert_time.day, cert_time.hour, cert_time.minute, cert_time.second)
return self.file_date
def zip_write(self, zip_file, file_name, data):
zip_info = ZipInfo(file_name, date_time=self.get_cert_date())
zip_info.compress_type = ZIP_DEFLATED
zip_info.create_system = 0
zip_file.writestr(zip_info, data)
def modify_apk(self, apk_name):
self.log('Modify libom.so...')
name_list = os.path.splitext(apk_name)
dest_name = name_list[0] + '-mod' + name_list[-1]
apk_file = ZipFile(apk_name, 'r', ZIP_DEFLATED)
dest_file = ZipFile(dest_name, 'w', ZIP_DEFLATED)
mf_file_data = 'Manifest-Version: 1.0\r\nCreated-By: 1.0 (Android SignApk)\r\n\r\n'
sf_file_data = ''
for item in apk_file.infolist():
if not item.filename.startswith('META-INF'):
item_data = apk_file.read(item.filename)
if item.filename.endswith('libom.so'):
item_data = self.opm_patch(item_data)
mf_entry, sf_entry = self.get_hash_entry(item.filename, item_data)
mf_file_data += mf_entry
sf_file_data += sf_entry
self.zip_write(dest_file, item.filename, item_data)
self.log('Sign and align package...')
sf_file_data = "Signature-Version: 1.0\r\nCreated-By: 1.0 (Android SignApk)\r\nSHA1-Digest-Manifest: %s\r\n\r\n%s" % (self.sha1_hash_base64(mf_file_data), sf_file_data)
rsa_file_data = self.smime_sign(sf_file_data)
self.zip_write(dest_file, 'META-INF/MANIFEST.MF', mf_file_data)
self.zip_write(dest_file, 'META-INF/CERT.SF', sf_file_data)
self.zip_write(dest_file, 'META-INF/CERT.RSA', rsa_file_data)
dest_file.close()
apk_file.close()
self.log('Patch done: %s' % dest_name)
def main():
print('Opera Mobile Off-Road Proxy Modifier v%s' % OperaPackageTuner.VERSION)
print('Copyright (c) 2013 Linus Yang <laokongzi@gmail.com>\n')
parser = OptionParser('Usage: %prog [options] [Opera Mobile apk file]')
parser.add_option('-d', '--httpdomain', dest="http_domain", help="specify HTTP protocol proxy domain")
parser.add_option('-s', '--socketdomain', dest="socket_domain", default=None, help="specify Socket protocol proxy domain (default is the same as HTTP domain)")
parser.add_option('-p', '--httpport', dest="http_port", type="int", default=80, help="specify HTTP protocol proxy port (default is 80)")
parser.add_option('-k', '--socketport', dest="socket_port", type="int", default=1080, help="specify Socket protocol proxy port (default is 1080)")
(options, args) = parser.parse_args()
if len(args) < 1:
parser.error('no apk input file')
if options.http_domain == None:
parser.error('no HTTP domain specified')
tuner = OperaPackageTuner(options.http_domain, options.socket_domain, options.http_port, options.socket_port)
tuner.modify_apk(args[0])
if __name__ == '__main__':
main()
@shellexy
Copy link

shellexy commented Oct 6, 2015

您的 libom-patch-py 用来修改 opera 28 和 opera mini 8.0~9.0 后还是正常使用,
但是新版 opera mini 11.0 上修改了服务器后,启动就总是说网络无法连接了,反复重试也没有用。

能请 @linusyang 有空看看 opera mini 11.0 吗,谢谢啦

这儿有多个版本的 opera 和 opera mini
ftp://ftp.opera.com/pub/opera/android/mini/
ftp://ftp.opera.com/pub/opera/android/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment