simpleapples

重装过100次XP,打过10年红警。

「贪吃蛇大作战」的刷分尝试

| 评论

昨天同学推荐了一个最近比较火的游戏「贪吃蛇大作战」,今天已经到了AppStore总榜第二的位置。这两天一有空就会掏出手机玩几盘,不过无尽模式玩到3000分左右就遇到了瓶颈,于是怀着单纯的目的想研究下这个游戏的接口…

图1

使用Charles抓取请求

既然是研究接口,第一步就是看看接口的URL和参数返回都是什么了,请出神器Charles。

图2

通过Charles可以清楚地看到,游戏提交分数的接口和参数,至于返回,貌似没什么用。接下来仔细研究下请求,有几个参数能比较容易的判断他们的含义和作用。

1
2
3
4
5
6
7
8
9
10
11
12
device_id: 设备ID
game_mode: 1是无尽模式 2是限时模式
kill: 击杀次数
length: 蛇的长度
market: 我抓的是iOS设备发的请求,所以这里是apple
platform: 抓到的请求里是1,还不太明白具体的意思
push_channel: 也是1,不明白意思
push_id: 固定值111111111222222223333333344444444,后面逆向apk也证明了这一点
sid: 每次登录都会更新
snake_sign: 签名
uid: 用户ID
version: 我使用的版本是固定值2.1

通过逆向APK获取签名算法

明确了参数的意思,接下来就可以伪造请求了,不过在伪造请求之前,还有一个参数比较麻烦,那就是snake_sign,请求签名,从图中snake_sign的内容看,似乎有点像是一个base64过的东西,不过base64decode发现,根本不可读。与其猜签名算法,不如考虑一下其他办法。

「贪吃蛇大作战」还有安卓版本,并且引导下载的页面提供了apk,于是下载apk,使用jadx-gui进行逆向,使用jadx-gui打开apk后,代码一览无余。

通过查看代码,可以清楚的看到snake_sign字段的签名逻辑,首先将请求中所有参数按ASCII码顺序排列拼接成字符串,然后在字符串前添加POST& + [URL Path],添加后使用Key进行SHA1加密,key也可以在代码中获取到。对加密生成的结果,需要做一次base64,才是最终snake_sign字段的结果。

1
POST&top_list_v2/update_score&device_id=XXX&game_mode=1&kill=1&length=35&market=apple&platform=1&push_channel=1&push_id=111111111222222223333333344444444&sid=XXX&uid=XXX&version=2.1

图3

关于「贪吃蛇大作战」的一些猜测

签名方法已经获取到,本以为可以安心刷分了,但是只成功刷了一次4000分160击杀。猜测可能进入排行版需要人工审核,一个理由是刷完后过了几分钟才显示到排行版,另一个理由是,刷完4000分169击杀后,很快刷了一个高的离谱的分,但是并没有显示在排行榜,后面无论刷多少分都再也没有显示出来过,怀疑可能被人工屏蔽。

另外整个游戏和API的交互只有登录、获取用户信息、获取排行版最高分、提交分数等几种,所以游戏也不是真正的实时。

图4

在阿里云CentOS7中配置基于Nginx+Supervisor+Gunicorn的Flask项目

| 评论

需要在阿里云的CentOS7中搭建Flask应用的生产环境,记录一下。

配置Centos7

root登录后,首先新建一个普通用户并设置密码

1
2
adduser user
passwd user

接下来将用户的公钥复制到~/.ssh中,命名为authorised_keys,修改/etc/ssh/sshd_config禁用ssh中的root登录,修改默认ssh端口,并使用证书登陆,修改如下内容

1
2
3
Port 65535
PasswordAuthentication no
PermitRootLogin no

配置完成后重启ssh服务

1
systemctl restart sshd.service

CentOS7中用firewalld替换了iptables,需要手动将80端口和修改后的ssh端口添加到firewalld中

1
2
3
firewalld --add-port 80/tcp --permanent
firewalld --add-port 65535/tcp --permanent
firewalld --reload

配置Nginx

yum中可以直接安装nginx

1
yum install nginx

安装好后在/etc/nginx/default.d中添加location的配置,并指向8001端口,以后Flask会监听8001端口

1
2
3
location / {
  proxy_pass http://127.0.0.1:8001
}

配置好后重新载入nginx配置

1
systemctl reload nginx.service

安装Python

CentOS自带Python2.7,如果使用Python3,需要单独安装。

1
2
3
4
5
6
wget https://www.python.org/ftp/python/3.4.3/Python-3.4.3.tgz
tar xf Python-3.4.3.tgz
cd Python-3.4.3
./configure --prefix=/usr/local --enable-shared
make
make altinstall

接下来在项目中搭建虚拟环境,Python2虚拟环境使用virtualenv安装(使用pip install virtualenv命令安装),Python3环境使用pyvenv安装(Python3自带pyvenv),以Python3环境为例,在Web项目中,建立虚拟环境文件夹venv

1
pyvenv venv

接下来在项目路径下启用虚拟环境

1
source venv/bin/active

退出虚拟环境使用Ctrl+C或deactive命令

安装配置Gunicorn

Gunicorn使用pip install gunicorn安装,注意需要在虚拟环境中使用pip安装,这样才对应虚拟环境中的Python版本。安装好后,新建一个Gunicorn的配置文件,比如deploy_config.py,加入内容如下

1
2
3
4
5
6
7
8
import os
bind='127.0.0.1:8001' #绑定的端口
workers=4 #worker数量
backlog=2048
debug=True
proc_name='gunicorn.pid'
pidfile='/var/log/gunicorn/debug.log'
loglevel='debug'

保存文件后在虚拟环境中使用Gunicorn尝试启动

1
gunicorn -c deploy_config.py myapp:app

myapp是入口Python文件名,app是函数名。如果输出worker相关信息,表明启动成功。

安装配置Supervisor

yum可以直接安装Supervisor,需要注意的是Supervisor只支持Python2,所以不要在虚拟环境中使用pip安装supervisor。

1
yum install supervisor

安装后,在/etc/supervisord.d中建立配置文件xxx.ini,内容如下

1
2
3
4
5
6
7
[program:xxx]
command=/var/proj/xxx/venv/bin/python /usr/bin/gunicorn -c /var/proj/xxx/deploy_config.py myapp:app
autorstart=true
directory=/var/proj/xxx
autorestart=true
startsecs=10
startretries=20

xxx是项目名称,注意command中最好都写全路径,以区别系统环境和项目虚拟环境。完成后启动supervisord,使配置生效

1
supervisord -c /etc/supervisord.conf

在viewDidAppear中PushViewController失败的问题

| 评论

需要在ViewController(FirstViewController)的viewDidAppear中Push另一个ViewController()SecondViewController),于是使用如下代码:

- (void)viewDidAppear:(BOOL)animated 
{
    [super viewDidAppear:animated];

    UIViewController *secondViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"secondViewController"];
    [self.navigationController pushViewController:secondViewController animated:YES];
}

在iOS8中,这段代码工作良好,当FirstViewController出现时,由于立刻Push了SecondViewController,即使animated参数是YES,Push的动画都没有显示出来。但是在iOS7中却出现了不一样的情况,pushViewController方法似乎没有执行,SecondViewController也没有被推出。打断点可以发现pushViewController方法是被执行了的,但是界面上没有任何效果。

由于pushViewController方法是在viewDidAppear中被调用的,会不会是因为viewDidAppear时FirstViewController还有什么UI上的动作没有处理完,导致立即调用pushViewController失败?那么将pushViewController放入dispatch_async中应该就能解决这个问题。使用如下代码实现:

- (void)viewDidAppear:(BOOL)animated 
{
    [super viewDidAppear:animated];

    UIViewController *secondViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"secondViewController"];
    dispatch_async(dispatch_get_main_queue(), ^{
        [self.navigationController pushViewController:secondViewController animated:YES];
    });
}

使用GCD后,SecondViewController被成功的Push了出来。虽然不能确定,但是也在一定程度上印证了上面的猜测。

使用TestFlight进行应用的Beta测试

| 评论

TestFlight已经被Apple集成到iTunes Connect中,现在使用TestFlight可以很方便的进行应用的Beta测试。下面就来介绍一下如何使用TestFlight进行测试,已经其中的一些小问题。

首先需要在iTunes Connect中启用TestFlight,可以针对每个App的某一版本,决定是否启用TestFlight。进入iTunes Connect –> My Apps –> 某个App –> Prerelease,在上传的Build右上角,打开TestFlight的开关,接下来就针对这个版本启动了TestFlight。

图1

有了可以测试的应用,接下来还要有测试用户,TestFlight测试用户有两种,一种是内测用户InternalTester,最多25个,一种是公测用户ExternalTester,最多1000个,内测用户需要首先成为iTunes Connect User,而公测用户只需要知道他的Apple Id就可。要进行公测需要先经过Apple Review团队的审核。我们以添加一个内测用户为例,进入iTunes Connect –> Users and Roles –> iTunes Connect Users,首先添加一个iTunes Connect User,之所以要添加iTunes Connect User,是因为TestFlight的测试用户必须是一个iTunes Connect User,并且这个用户的角色必须是Admin或者Technical。添加后,用户的邮箱里会收到一封邀请邮件,点击邮件中的链接可以激活成为iTunes Connect User。进入iTunes Connect –> Users and Roles –> TestFlight Beta Testers,激活的iTunes Connect User会出现在这里,选中点击右上角的保存,这个用户就成为一个内测用户了。

图2

有了内测用户和测试App,下一步就要将两者关联了,进入iTunes Connect –> My Apps –> 某个App –> Prerelease –> Internal Testers中,这里会显示可用的内测用户,勾选用户并点击右上角的Invite,Apple就会给这个邮箱发送一封邀请邮件。接下来需要在要测试的手机上安装TestFlight,安装好后,TestFlight会自动绑定当前手机的Apple Id登录,这里你会发现TestFlight里并没有出现测试App,经过测试,必须要在iOS的自带邮件客户端上点击邀请邮件中的链接,才会跳转到TestFlight中安装应用。这是TestFlight让人非常不爽的一点。你可以将收到的邀请邮件转发到自带的邮件客户端所绑定的邮箱上,让后在自带邮件客户端中点击邀请链接打开TestFlight。

图3

点击链接后会自动跳转到TestFlight中,点击Install安装应用,再次进入TestFlight中就可以看到已安装的测试应用了。

iOS8中LaunchImage和LaunchScreen的完美结合

| 评论

Apple在iOS8中推出了LaunchScreen.xib来代替之前的LaunchImage作为程序的启动界面,相比与LaunchImage,在iOS设备屏幕尺寸越来越多样的情况下,LaunchScreen.xib依托AutoLayout无疑更方便,否则,对于一个兼容iPhone5-iPhone6Plus的应用,就需要有4长不同尺寸的LaunchImage。

但是LaunchScreen只有在iOS8中才能被支持,所以一些开发者还是选择用传统的LaunchImage方式。不过,还有一种方式是将LaunchImage和LaunchScreen结合,在大尺寸iPhone中使用LaunchScreen(iPhone6和iPhone6Plus都是iOS8系统),在iOS7中使用LaunchImage(使用iOS7的手机只有4寸和5.5寸的iPhone,所以只需要两张图)。

首先进入Target配置,找到App Icons and Launch Images,Xcode6中默认使用了LaunchScreen.xib,而LaunchImage则没有使用。

图1

接下来点击Use Assets Catelog,这时Xcode会在Images.xcassets中生成LaunchImage,这里我们只需要给4寸Retina屏和3.5寸2x屏放两张LaunchImage就好了。

在iOS8中系统会优先调用LaunchScreen作为启动界面,而iOS7不支持LaunchScreen则会使用LaunchImage中的图片作为启动界面。

图2

项目在iOS7模拟器中运行效果(使用LaunchImage)

图3

项目在iOS8模拟器中运行效果(使用默认的LaunchScreen.xib)

创建一个简单的Safari扩展

| 评论

之前做过一个把网址转为二维码的Chrome扩展,想在Safari中也使用这样的扩展,搜索了一下貌似没有同类型的,所以自己打造了一个Safari扩展,并且把过程记录下来。

获得开发者证书

要建立一个Safari扩展,首先需要生成一个开发者证书。访问Apple开发者中心,加入Safari Developer Program,加入开发者计划是免费的。加入开发者计划后就可以生成证书了,访问Certificates, Identifiers & Profiles,点击右上角的加号,生成一个开发者证书。成功后将证书下载到本地并导入Keychain Access中。

在Safari中创建扩展

打开Safari,在菜单中选择Safari—Preferences-Advanced,勾选最下方的Show Develop menu in menu bar,如图。

图1

这时在菜单栏中会出现Develop菜单,选择Develope-Show Extension Builder,打开扩展编辑器,点击左下角的+,选择New Extension,保存到一个位置(例如Desktop)。

图2

这时,Desktop文件夹中会出现一个demo.safariextension文件夹,这个文件夹里的内容就是我们生成的Safari扩展的根目录。如果前面的开发者证书已经正确导入,扩展的介绍里会出现Safari开发者的Id。

图3

编辑扩展基本信息

扩展建立后,下面会有一票东西需要填写,首先填写扩展的基本信息。

  • Display Name: 扩展的显示名称
  • Author: 作者名字
  • Description: 插件介绍
  • Website: 插件网站
  • Bundle Identifier: 这里需要填写唯一id
  • Update Manifest: 这里需要填写一个plist格式文件的地址,Apple会定期检查这个地址中的Version,如果有升级,就会访问插件的下载地址更新插件,当然,如果插件只是自用而不提交Safari Extensions Gallery的话,这一栏可以不填,plist文件格式如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
   <key>Extension Updates</key>
   <array>
     <dict>
       <key>CFBundleIdentifier</key>
       <string>com.zangzhiya.url2qrcode</string>
       <key>Developer Identifier</key>
       <string>开发者ID</string>
       <key>CFBundleVersion</key>
       <string>1</string>
       <key>CFBundleShortVersionString</key>
       <string>1.0</string>
       <key>URL</key>
       <string>http://simpleapples.com/upload/url2qrcode-safari/url2qrcode.safariextz</string>
     </dict>
   </array>
</dict>
</plist>
  • Access Level: 这里需要选择插件对页面访问的权限,比如我们我们要做的URL转二维码插件,需要获取当前Tab的URL,那么这里需要选择ALL

图4

添加图标

我们的扩展到现在还没有图标,Safari会分配一个默认的指南针图标,添加图标的方式很简单,在扩展的根目录下放置一个16px * 16px的png格式图片,Safari就会自动将其置为图标。

添加Toolbar Item和Popovers

首先看一下这个扩展的完成态,如图。

图5

可以看到这个插件有两部分组成,一个是工具栏的按钮,在Safari中被称为Toolbar Item,一个是点击按钮后弹出的层,是一个Popover,和Chrome中的Popup类似,这个Popover也是一个html页面。

接下来,需要在Safari Extension Builder中继续编辑,添加一个Toolbar Item和一个Popover,如图。

图6

Toobar Item红的Image必须是一个8bit的16px * 16px透明背景的黑白png图像(繁琐的要求),而Popover需要是一个html文件。这里的路径都是相对于扩展文件夹的。到这里插件的配置工作就完成了。

编程

可以在我的Github上查看这个扩展的代码,里面用到的Safari Api(获取当前页URL),可以在Safari Developer Libray中找到,里面的内容非常详细。

打包

当扩展开发完成后,就可以点击Safari Extension Builder中的Build Package打包了,打包出来的会是一个safariextz格式的文件,双击就可以安装。如果不想提交Safari Extension Gallery,可以直接把这个文件拷贝给他人安装。

图7

iOS8注册推送失败的探究

| 评论

Apple在9月18日正式发布了iOS8,在收到更新的同时,也发现自己的应用在iOS8下无法启动。 并且在Console中收到如下提示:

1
2014-09-19 16:26:20.369 demo[379:30506] registerForRemoteNotificationTypes: is not supported in iOS 8.0 and later.

查询文档可以知道,在iOS8中注册推送的方法registerForRemoteNotificationTypes已经被废弃,文档中是这样描述的:

Alt text

按照文档中的提示,使用registerForRemoteNotifications方法代替,这个方法不接受参数。紧接着问题就来了,程序在安装过这个应用的iOS8机器上可以成功注册,而在新安装的iOS8机器上则无法注册application:didRegisterForRemoteNotificationsWithDeviceToken:application:didFailToRegisterForRemoteNotificationsWithError:都无法响应,并且在成功注册的机器上,收到的推送消息也没有声音提示。再次查看文档后发现这么一段话:

1
If you want your app’s push notifications to display alerts, play sounds, or perform other user-facing actions, you must call the registerUserNotificationSettings: method to request the types of notifications you want to use.

换句话说,如果要使用推送服务,还需要再调用registerUserNotificationSettings方法,而这个方法是和iOS7上的registerForRemoteNotificationTypes方法一样接受参数的。为什么iOS8下要把一个方法变成两个方法呢?registerForRemoteNotifications方法的文档中有这么一句话:

1
Call this method to initiate the registration process with Apple Push Service. If registration succeeds, the app calls your app delegate object’s application:didRegisterForRemoteNotificationsWithDeviceToken: method and passes it a device token.

苹果可能考虑到一些应用注册提醒可能只是为了获取deviceToken,所以将获取deviceToken单独提成一个方法,而要接收推送还需要单独调用别的方法,这样就细化了逻辑,方便不用的开发需求。不过在真机调试中,一台刚重置过的搭载iOS8系统的iPhone5c,调用registerForRemoteNotifications方法后,并没有收到任何失败或成功的回调,和Apple文档中描述的不符,怀疑是SDK在这部分也有bug。

下面贴一下兼容iOS7和iOS8的注册推送代码:

1
2
3
4
5
6
7
8
9
10
11
#define IS_OS_8_OR_LATER ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.0)
if (IS_OS_8_OR_LATER) {
    [[UIApplication sharedApplication] registerForRemoteNotifications];
    UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes: (UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeSound) categories:nil];
    [[UIApplication sharedApplication] registerUserNotificationSettings:settings];
} else {
    [application registerForRemoteNotificationTypes:
     UIRemoteNotificationTypeBadge |
     UIRemoteNotificationTypeAlert |
     UIRemoteNotificationTypeSound];
}

Air SDK和Flex SDK的区别

| 评论

最近将AS开发的SDK从Flex SDK 4.9.1升级到了Air SDK 4.0,导致之前写的一个FLV播放器不能播放视频了,最后发现bug是一个方法内的变量名和类中的一个get方法重名了。类似下图中所示情况:

Alt text

图中变量time和get方法time重名,而编译器对这种重名的处理在Flex SDK和Air SDK下是不一样的。

Flex SDK下,会根据上下文将var time:int = time;中的第二个time作为get方法,而Air SDK中,这行两个time会被当作同一个变量,也就是自己等于自己,而int类型定义时候会被初始化为0,所以使用Air SDK输出结果为0,使用Flex SDK输出结果为1000。

看来Air SDK不如Flex SDK智能啊 :)

Eclipse 4.3安装Flash Builder Plugin

| 评论

Flash Builder是一个基于Eclipse的IDE,在Flash Builder文件夹下的utilities目录下,官方已经为我们提供了插件版的安装程序,名为Adobe Flash Builder 4.7 Plug-in Utility,插件版可以将Flash Builder嵌入到已经安装好的Eclipse中,作为Eclipse的一个视图,这样就可以在Eclipse中开发AS程序。

执行插件版安装程序后按提示进行,Eclipse目前的最新版是4.3,而Flash Builder 4.7的插件版只支持Eclipse 3.7或4.2版,无法安装在4.3中。

Alt text

解决办法是,下载Eclipse 4.2,现将Flash Builder插件版安装到4.2版的Eclipse中,再拷贝Eclipse文件夹下的dropins目录覆盖Eclipse 4.3的dropins。启动Eclipse 4.3,第一次启动速度会很慢,启动后,Flash Builder插件版就已经安装到Eclipse 4.3中了。

Javscript中Object的Key

| 评论

先来看一段代码:

1
2
3
4
5
6
7
8
9
10
<div id="e1"></div>
<div id="e2"></div>
<script>
  var c1 = document.getElementById("e1");
  var c2 = document.getElementById("e2");
  var obj = {};
  obj[c1] = 1;
  obj[c2] = 2;
  console.log(obj);
</script>

上面这段代码会输出什么结果呢?答案是:

1
Object {[object HTMLDivElement]: 2}

为什么给了obj两个元素,输出出来却变成了一个呢?难道c1和c2指向同一个元素?首先c1和c2是两个不同的dom节点,并且通过输出c1 === c2会发现结果为false,而输出obj[c1] === obj[c2]的结果却为true。通过上面结果可以初步推断,c1和c2作为object的key时,值可能是一样的。object的key所接受的值只有string一种,所以当c1这个dom元素作为key时,会被转换为string类型,而c2和c1由于都是div,所以转换成string后,值都是一样的,c2作为key也会覆盖c1。导致出现只有一个元素的结果。如果c2元素变成其他类型的dom节点,转换为string类型时就会不一样,例如将c2改为ul,还是上面的js代码,输出结果为:

1
Object {[object HTMLDivElement]: 1, [object HTMLUListElement]: 2}

当使用object时,需要注意key的类型会被转换为string。