#!/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() |
![]() |
1
hewigovens 2014-02-03 13:58:28 +08:00
lz在家看来没闲着, 自己搭opera mini的估计比ss的人还少
|
![]() |
2
lovejoy 2014-02-03 16:04:07 +08:00
@hewigovens 是啊,不如挂个代理了。
|
![]() |
3
zxy 2014-02-03 16:26:25 +08:00 via Android
昨晚建了pptp.
|
![]() |
4
linusyang OP @hewigovens 好久以前写的,因为我一直用 Opera Mini 的,从 S40 时代开始。。 :)
@lovejoy 搭建只要两行: iptables -t nat -A PREROUTING -p tcp --dport 自定义端口 -j DNAT --to-destination 141.0.11.253:1080 iptables -t nat -A POSTROUTING -p tcp --dport 1080 -j SNAT --to-source 你的IP |
![]() |
5
tamamaxox 2014-02-04 10:20:08 +08:00 via Android
那。。。有Linux版吗
|
9
JohnChu 2015-02-14 00:10:38 +08:00
LZ这样子签名被换掉,升级就有问题了啊
|
![]() |
16
Khlieb 2016-07-16 14:51:10 +08:00 via Android
楼主能不能搞个能让 Firefox 用上这个服务器的扩展?上 MDN 网站能找到开发工具。
|