2015年5月26日星期二

不要在为唯一的port号烦恼了

好多项目里,在Android的App里做个socket server让外面用,port选多少呢? 往往会固定一个自以为没人用的,或者允许配置。

多少年了,还这么不严格,不能更简单点吗?

可以了,因为Android里一直是支持Abstract Socket Name的,就是用任意的string来代替port。

老式的写法:
ServerSocket srv = new ServerSocket(portNumber)

与时俱进的例子: (那也是相对于2008年了)
ServerSocket srv = new LocalServerSocket("a.b.c");
参数其实随便什么string都行,这总好找吧,而且免除配置了。
至于Socket client什么的,自己照照,写法也是改一句。
不仅仅是名字好决定,而且速度也比port那种快(因为没有走tcpip那套,直接就是共享内从倒腾一下)。


剩下来还有一点,如何从PC那边通过port forward连接到Android里的这个LocalServerSocket呢,adb forward命令支持吗?

adb forward支持的, run:
adb forward tcp:8000 localabstract:a.b.c

顺便,如果是Linux/Unix/Mac系统,那么本地也可以使用类似的socket,
例如
adb forward localfilesystem:/tmp/a.b.c localabstract:a.b.c

那么本地就会出现一个文件/tmp/a.b.c, socket就是基于这个名字通过共享内存来高效通信。至于如何连上本地这个socket,那看情况了,Java,socat,nc, python等工具都支持。

还有一点有点扯远了,就是连这个port forward都不要预先建立了,直接通过5037端口连上Android里面的socket。据我的经验这样速度居然提高不少(很久前做的实验,也可能有其他因素,结论不严格)。大致的方法有感兴趣的以后再贴。别的地方都有介绍,例如nodejs实现的adbkit,一大堆。说大白话就是两步:
1. 向localhost:5037 发送host:transport:ANDROID_DEVICE_SERIAL_NUMBER    得到回答,如果是OKAY四个字那就到step2,否则就错误。
2. 继续向上述port发送localabstract:a.b.c
    得到回答,如果是OKAY四个字那就到step2,否则就错误。
然后这个链接一直保持着,用来和Android那边直接通信。
只有一点需要注意,由于Android里面的adbd设计的比较傻,两步的内容不能一起发送过去!,一定要等到第一步的OKAY以后才发第二部,不然他内部还没有把通路切换到目标socket那边。

最后提一句,在C里当然一样可以使用这个功能,其实这就是Unix Domain Socket的一种Linux下的进化。

HTTP请求中,IF-MODIFIED-SINCE造成的问题

有个调查,要搞清楚为什么IE8访问特别慢,IE11和别的Browser都没问题。
环境是Apache+WebLogic做的一个Login网页,瞬间就完了的事儿,IE8却耗费30秒才出来,就算是从本地访问都是。

简单的调查例如IE兼容模式,IE cache,检查server配置等都有人做过了,没有明显的线索。

用IE8的developer tool抓不到网页的第一个请求内容,apache那边的Access Log也没有配置成现实响应耗费时间。

也没多想,还是老方法,socat神器登场,做个假server做转接,打印出来往内容,就可以知道更多,比抓包的好看。(不过windows下的socat似乎不怎么方便安装,我还是安装cygwin里在net分类里选择的socat和netcat(nc))。

Run:
socat -D -v -tcp-listen:localhost:8000 tcp-connect:RealServerIP:80

(-D选项是为了现实新出现的连接,为了保险起见看看)

Browser:
http://localhost:8000/

于是socat现实一大堆HTTP request header:
GET / HTTP1.1 ......
.......
IF-MODIFIED-SINCE: .....某个诡异的2010年的日期.....   //这个意思就是问server这个网页在这个日期之后又无更新


而Response呢,三十秒后才来说:
HTTP 304
.....

这个就更诡异了,304就是说没有更新。那你早回答啊,需要这么长时间???

当然,一开始并没有觉察到IF-MODIFIED-SINCE有什么诡异。

后来用firefox和chrome都做了一下,对比一看,人家没有问IF-MODIFIED-SINCE啊。

于是做个验证,

另一个老牌工具netcat登场 (不过后来又nmap公司新出品的ncat更强,但是不容易安装到),当时机器里么有telnet,所以我也只能用这个了,Windows发神经把telnet从默认安装选项里去掉了,几年了还不响应呼声加进来,太不自觉了。

Run:
nc localhost 8000

然后输入HTTP request header..... 最后是个空回车,才会被server接受,
的确是一旦有了IF-MODIFIED-SINCE,就很慢。

暂时到这儿了,至于为啥慢那就是apache那边犯傻了,该咋改就咋改。

话说好奇为啥IE8要问IF-MODIFIED-SINCE啊,其他的Browser都不问?不可能,肯定是server那边第一次返回response时指出了LAST-MODIFIED-DATE才让browser误解的,

看了看response内容的确如此。

那最后推断,如果把browser的cache都清除,肯定第一次会快,第二次就会慢。

Chrome:  推论正确
IE:  推论正确
   但是,不是那么简单的就能够清除的,首先要把IE彻底关掉,然后从Control pannel的Internet Option里,清除cache。和Browser里的清除的方法一样。
   另外,为了保险起见,还是动用了Process Monitor神器来过滤找到cache的文件在什么目录,现在忘了,反正就是AppData下的什么一个Content.IE5目录下,把这个目录干掉就万无一失了。

Firefox: 版本特殊无法证实。安装上的firefox是强制被设定成不保存cache的,甚至访问履历都没有,一旦关闭就什么都没了。这也许是管理上的policy。没做多研究。
但是从另一个反面得到证实:这个版本的firefox从来不发IF-MODIFIED-SINCE,不管是第几次访问。






WebSocket在HTTP Proxy下是可以,可是有点小笨拙之处给人造成不便

从规格说明上看,WebSocket毋庸置疑是可以通过HTTP Proxy Server代理访问出去的,不论是非加密的ws://,还是加密的wss://,都应该可以。当然有个通常都满足的前提:HTTP Proxy Server提供socket连接级别的无过滤转接,是所谓的CONNECT命令(而不光是HTTP整体请求响应的转接和过滤)。现实因为如果不提供这个CONNECT命令支持,那么HTTPS网站就用不了(原因一句话说不清,当然通过伪造SSL证书等手段也是可以的,只是需要browser机器这边配合)。总之,HTTP Proxy Server从原理上看,一旦支持了CONNECT命令,那么他后来就对连接中发生的加密内容基本上就是干瞪眼,无法区分到底是不是HTTPS了,从某种意义来说,这是HTTP Proxy Server设计上的一种漏洞。

事实上的确我使用WebSocket一段时间,和http proxy配合也正常,wss(加密)和ws(不加密)两种协议都行,

可有一天在一个http proxy环境下发生问题了!

经过一番疑神疑鬼的调查,最终确定是自动配置script里没有对wss类型提供proxy server ip和port,而只是回答了一个direct (就是不经过proxy server直接连接),所以失败了。

对策很简单,修改pac或者配置一个固定的proxy的ip,port。

memo一下调查经过:

所用工具:socat

在Firefox/Chrome/IE里,使用加密型WebSocket: new WebSocket('wss://x.y.z/test'),爆出错误说timeout, 而非加密型 new WebSocket('ws://x.y.z/test') 也报错。

Proxy 配置:

自动: http://proxyserver/config.pac

调查开始:

先搞清楚这个config.pac干什么的,其实是个根据url, host来回答该用什么proxy server的东西,而且是个javascript。例如:

function FindProxyForURL(url, host) {
  if (url.substring(0, 5) == "http:") {
  return "PROXY proxy:80";
} else if (url.substring(0, 6) == "https:") {
  return "PROXY secproxy:8080";
} else {
  return "DIRECT";
}
}

那看了一下就是用secproxy:8080了啊,https都正常访问了,为啥wss出毛病?

当时绕了个弯子,想要看看http proxy内部发生了什么,于是就拿socat工具来做个中转来分析,这是个神器,以后在写一些活用法。

Run:
socat -v tcp-listen:8080,reuseaddr,fork  tcp-connect:secproxy:8080

就是用来做一个假的proxy server,本地的。其中的动作都转接到真的proxy server里去,
然后设定Browser通过这个假的Proxy server访问,于是可以看到其中的来往数据,

内容就省略了。着么折腾了一下也忘了抓取屏幕,忘了是怎么搞的,似乎是server对Websocket的握手请求作出回答后,browser就没有继续请求了,反正意识到自签名的证书会导致问题:WebSocket Server那边的server ssl证书是自签名的,那么可能这边不承认,嗯,的确有人说是,那好,我把证书给作为[可信赖的顶级证书发型者]来加入到系统,哦,https访问网站时的叉号消失了。

着么搞一下,的确wss可以了。

但是回到真环境,发现还是不行,最后用Sysinternal's Process Monitor工具追踪网络调用,发现browser居然不通过proxy就直接出去,于是出错了。

去掉自动配置选项,改成固定的proxy的ip,port就好了。


最终,我觉得websocket在选择Proxy server的方法上做的有点傻?居然直接傻乎乎问wss://somesite 该用什么proxy server,而不是聪明的问https://somesite该用什么网站。



2015年5月25日星期一

Android 的锁屏画面 居然可以无需密码就解除?我凌乱了。

做个memo。
偶然发现Android App可以自己解除锁屏画面,哪怕是有密码的,当然,前提是要把这个App启动起来。

Source & APK, 摘要:

wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
ghostView = new View(this);
lp = new LayoutParams();
lp.type = LayoutParams.TYPE_SYSTEM_ERROR;
lp.width = 0;
lp.height = 0;
lp.flags = 0;
lp.flags |= LayoutParams.FLAG_NOT_FOCUSABLE;
lp.flags |= LayoutParams.FLAG_NOT_TOUCHABLE;
lp.flags |= LayoutParams.FLAG_SHOW_WHEN_LOCKED;
lp.flags |= LayoutParams.FLAG_TURN_SCREEN_ON;
lp.flags |= LayoutParams.FLAG_KEEP_SCREEN_ON;
lp.flags |= LayoutParams.FLAG_DISMISS_KEYGUARD;

wm.addView(ghostView, lp);  //这是做了一个最顶层的悬浮无焦点的幽灵窗口,迫使开亮屏幕。

keyGuardManager = (KeyguardManager) getSystemService(KEYGUARD_SERVICE);
keyGuardLock = keyGuardManager.newKeyguardLock(getPackageName());
keyGuardLock.disableKeyguard();  //缺了这个就不能解锁了。


安装了之后就可以启动service
adb shell pm start asc.tool.passwordforgotunlocker/. PasswordForgotUnlockerService --user 0


这就可以挪走密码锁定屏幕了,然后可以任意操作app,除了一点:不能按HOME按钮。当然,可以通过BACK键退回到Home画面,然后启动任何app都行。


于是我就想着,设法让这个app一旦安装就自动执行,那 发布这个app到play store上,然后从PC的play store网页里向android device安装(前提是device里加入了相同的goole account),这样岂不是就可以解锁了?有点荒唐?

验证了一下,发现以前2.3时代能够静态登陆的Broadcast Event Receiver方式不管用了,
例如子AndroidManifest.xml里加入
<receiver android:name="asc.tool.passwordforgotunlocker.MyReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.SCREEN_ON" />
<action android:name="android.intent.action.TIME_TICK" />
</intent-filter>
</receiver>
都不行了,有人验证过后两个event非得手动通过执行registerReiver api来注册,但是第一个BOOT_COMPLETED居然也不行,我很惊讶,难道google特意这样?这个app是没有任何luancher icon的,安装完了也不会在HOME里有任何显示。没兴趣研究了。

更新:Android 5.0开始不能这样绕过密码输入了。

---------------------------------------------------------------------------------------------------
顺便发现在Play store里有个Android Device Manager早就做了这些了,还不止,还可以定位,振铃,删除数据。开始像是偷偷做的,因为Android 2.3位置,都没有明确的设定告诉用户有这些功能和提供选项。Android 4.x开始 好歹提供了一个Google Settings的app让人知道有这么个东西在起作用。稍微看了一下,发现这些功能似乎来自于系统内置的Android Developer Service里?

条件(方法):在Android手机里的Account设定里,加入一个google account(其实就是一个gmail地址就可以了。然后用这个gmail进入play store,到设定画面,执行Device Manager就可以。

当手机找不到的时候可以用这个。甚至可以在一台手机上安装一个Android Device Manager App来寻找另一台。

详细的请看 How to use Android Device Manager - AndroidPIT



2015年5月23日星期六

Android自动测试工具monkey还是不靠谱啊

详细的monkey源码在Source or jar
monkey名字真难听,

这东西有两种用法,一种是网络命令行模式
term1:
adb forward tcp:4444 tcp:4444
adb shell monkey --port 4444


nc localhost 4444
wake
type hello
quit


还有一种用法是script file模式
monkey.txt:
start data >>
DispatchString(hello)
RotateScreen(1,0)
UserWait(1000000)

adb push monkey.txt /data/local/tmp/monkey.txt
adb shell monkey -f /data/local/tmp/monkey.txt 1

这种用法里可以设定屏幕方向,内部是冻结方向感应器。
RotateScreen(1/*横向*/,0/*monkey退出后恢复自动旋转*/)
RotateScreen(1/*横向*/,1/*monkey推出后依然禁止自动旋转*/)

这两个方法都比较温柔,如果一个app明示自己只喜欢纵向,那么这个命令没有效果。

内部是调用了
  1. iwm.freezeRotation(mRotationDegree);
  2. if (!mPersist) {
  3. iwm.thawRotation();
  4. }

奇怪的是,在网络命令模式下,不接受rotate命令,原因可能是因为fressRotation一旦被调用了,monkey退出时才能够自动解除方向冻结,因为ibinder没有提供disconnect方法。

这个monkey不靠谱,在网络命令模式下还能够wake 屏幕,但是script file模式下的DeviceWakeUp() 命令明明是允许的,但是timeout,是因为他内部试图调用一个不存在的内部Activity: com.google.android.powerutil.WakeUpScreen


    if (s.indexOf("DeviceWakeUp") >= 0) {
      String pkg_name = "com.google.android.powerutil";
      String cl_name = "com.google.android.powerutil.WakeUpScreen";
      long deviceSleepTime = this.mDeviceSleepTime;

      ComponentName mApp = new ComponentName(pkg_name, cl_name);
      this.mQ.addLast(new MonkeyActivityEvent(mApp, deviceSleepTime));

      this.mQ.addLast(new MonkeyKeyEvent(0, 7)); //POWER key down
      this.mQ.addLast(new MonkeyKeyEvent(1, 7)); //POWER key up

      this.mQ.addLast(new MonkeyWaitEvent(deviceSleepTime + 3000L));

      this.mQ.addLast(new MonkeyKeyEvent(0, 82)); //MENU key down
      this.mQ.addLast(new MonkeyKeyEvent(1, 82)); //MENU key up

      this.mQ.addLast(new MonkeyKeyEvent(0, 4)); //ESC key down
      this.mQ.addLast(new MonkeyKeyEvent(1, 4)); //ESC key up

      return;
    }

这个方法真不靠谱,还要sleep, 不仅速度慢,而且依靠menu key(其他的键也可以)解除无密码的锁屏画面,要是当时已经解锁了,这个menu键就会导致app弹出一个菜单,或者app自定义的动作。怎么可以做得着么马虎呢?

monkey里有些event产生的方法例如PinchZoom可以借鉴。