2017年3月23日 星期四

道不同,不相為謀

每隔一段期間,同溫層又會彼此檢視著近況,近來越來越有強烈的孤僻感,或許就是「道不同,不相為謀」吧!

前陣子得知一些強者同事離職,細細品味後,稱得上在錯的時間遇上對的人,出現高頻的內耗,不得已只好分別。畢竟大多數的公司只需要完美執行老闆的策略,而非不斷跳出來 challenge 老闆的人,想著想著也感慨了起來。

除此之外,漸漸進入 35 歲的人生,偶爾哈拉起同好,倒不是每個人有強烈的求生意識的,不少如溫水煮青蛙,停滯了進展。但,沒有什麼是絕對的好或絕對的壞,永遠的事後話!在對岸發展的強者學弟,隨著公司角色的轉換,開始不 coding 了,卻也開始感到內心空虛寂寞覺得弱 XD 彼此寒暄後,人生就是一場場賭注罷了!只待小公司,若沒起色,那段就變成空白人生,若成了肯定比在大公司獲得更多;只待大公司,CP值、未來好推算,但往上的位置早已被卡位,容易成了永遠的螺絲釘。所以,只能彼此期許著未來是賭對的,繼續走下去了。

回到標題,我想,漸漸地眼不見為淨吧,為那些不同道的人心煩也挺浪費光陰的,給予什麼回應也無法清醒,那就都別說了,大家找著自己的同溫層,繼續黑皮下去,人生也沒什麼不好囉

2017年3月7日 星期二

使用 Facebook Graph API 取得使用者資料:性別、年紀、地區、教育來做問卷調查

幾年前開發 sign in via Facebook,然後就停擺了好一陣子,最近市場研究想做個問卷,才發現當年收集的資料不怎齊全 XD 再加上 Facebook Graph api 也改版幾次,並且越來越重視個資保護,有很多權限必須額外取得才行。

請參考 https://developers.facebook.com/docs/graph-api/reference/user 定義最準。

而一般問卷常用的年紀,在 Facebook 只有 13/18/21 這三種數值可用,要再更仔細去請用 FB ads 去發了;而教育資訊跟地區資訊也得額外要求額外的權限 user_education_history 跟 user_location 才能得到

最後,在用 graph api 來取得這些資訊吧:

/me?fields=id,name,email,education,gender,location,age_range
{
  "id": "##",
  "name": "Yuan-Yi Chang",
  "email": "@gmail.com",
  "education": [
    {
      ...
      "type": "High School",
      ...
    },
    {
      ...
      "type": "College",
      ...
    },
    {
      ...
      "type": "Graduate School",
      ...
    }
  ],
  "gender": "male",
  "location": {
    "id": "110765362279102",
    "name": "Taipei, Taiwan"
  },
  "age_range": {
    "min": 21
  }
}


預計先這般儲存吧:

age_range => 13/18/21
gender => None/Male/Female/Unisex
education => None/HighSchool/College/GraduateSchool

2017年3月2日 星期四

[PHP] Facebook Graph API v2.2 升級提醒 - CodeIgniter 2.x 與 Facebook PHP SDK v5

這幾天一堆 fb app 被這種信轟炸:YourFBApp 的新開發人員重要通知,簡言之就是 facebook graph 舊版 api 即將在 2017/03/25 失效,請立即更新。

Facebook 開放平台變更紀錄 - https://developers.facebook.com/docs/apps/changelog

追了一下,主因是很多手上的服務是在 2014 年底開發,當時就是用 v2.2 graph api 沒錯,關鍵之處可以用搜尋:

facebook-php-sdk-v4 $ grep -r "v2.2" *
src/Facebook/FacebookRequest.php:  const GRAPH_API_VERSION = 'v2.2';


非常精準的地發現自己就是這個族群,也感謝 Facebook 賜予工作機會(誤),這樣每兩年就有新工作可以做,非常開心!接下來就是痛苦的開始,還是一口氣從 v4 升級到 v5 吧!

前提:

由於使用 PHP CodeIgniter 2.x 架構,再加上 HA 架構,因此需要處理 SESSION 的儲存機制,在此就用 DB 處理,而對 v4 SDK 中,是直接在 FacebookRedirectLoginHelper 改寫一份對於認證所需資料儲存的東西,但在 v5 架構就更漂亮了,可以把負責資料儲存的東西傳入,不需再改成 FacebookRedirectLoginHelper。若服務仍在單一機器上運行,可以略過此設計。

v4:

<?php
// FacebookRedirectLoginCIHelper
namespace Facebook;
use Facebook\FacebookRedirectLoginHelper;
class FacebookRedirectLoginCIHelper extends \Facebook\FacebookRedirectLoginHelper {
private $sessionPrefix = 'FBRLH_';
public function __construct($redirectUrl, $appId = null, $appSecret = null) {
parent::__construct($redirectUrl, $appId, $appSecret);
$this->ci = & get_instance();
}
protected function storeState($state) {
if ($this->ci->session->set_userdata($this->sessionPrefix . 'state', $state)) {
$this->state = $this->ci->session->set_userdata($this->sessionPrefix . 'state', $state);
return $this->state;
}
return NULL;
}
protected function loadState() {
return $this->state = $this->ci->session->userdata($this->sessionPrefix . 'state');
}
}

v5:

<?php
// FacebookSessionPersistentDataCIHandler
namespace Facebook\PersistentData;
use Facebook\Exceptions\FacebookSDKException;
class FacebookSessionPersistentDataCIHandler implements PersistentDataInterface {
protected $sessionPrefix = 'FBRLH_';
public function __construct($enableSessionCheck = true) {
$this->ci = & get_instance();
}
public function get($key) {
if ($this->ci)
return $this->ci->session->userdata($this->sessionPrefix . $key);
return null;
}
public function set($key, $value) {
if ($this->ci)
$this->ci->session->set_userdata($this->sessionPrefix . $key, $value);
}
}


初始化:

v4:

require 'path/facebook-php-sdk-v4/autoload.php';
use Facebook\FacebookRedirectLoginCIHelper;
use Facebook\FacebookRequest;
use Facebook\FacebookSession;
use Facebook\GraphUser;

FacebookSession::setDefaultApplication($this->config->item("api_key"), $this->config->item("secret_key"));

v5:

require 'path/php-graph-sdk-5.4.4/src/Facebook/autoload.php';
use Facebook\Facebook;
use Facebook\Authentication\AccessToken;
use Facebook\PersistentData\FacebookSessionPersistentDataCIHandler;
use Facebook\FacebookRequest;

$fb = new Facebook([
'app_id' => $this->config->item("api_key"),
'app_secret' => $this->config->item("secret_key"),
'persistent_data_handler' => new FacebookSessionPersistentDataCIHandler(),
//'default_graph_version' => 'v2.8',
]);


取得登入網址:

v4:

$helper = new FacebookRedirectLoginCIHelper($callback_url);
$login_url = $helper->getLoginUrl($this->config->item('fb_scope'));

v5:

$helper = $fb->getRedirectLoginHelper();
$login_url = $helper->getLoginUrl($callback_url, $this->config->item('fb_scope'));


登入完成取得 access_token:

v4:

$fb_session = $helper->getSessionFromRedirect();
$access_token = $fb_session->getAccessToken();

v5:

$accessToken = $helper->getAccessToken();


取得 longlived access_token:

v4:

$long_lived_token = $fb_session->getLongLivedSession()->getToken();

v5:
$long_lived_token = (string) $fb->getOAuth2Client()->getLongLivedAccessToken($access_token);


查詢個人資料:

v4:

$user_profile = (new FacebookRequest($fb_session, 'GET', '/me'))->execute()->getGraphObject(GraphUser::className());
$uid = $user_profile->getProperty('id');
$name = $user_profile->getProperty('name');
$email = $user_profile->getProperty('email');
$profile_url = $user_profile->getProperty('link');
if (empty($profile_url))
$profile_url = https://www.facebook.com/$uid";
$profile_image_link = "https://graph.facebook.com/$uid/picture";

v5:

$user_profile = $fb->get('/me', $accessToken)->getGraphNode();
$uid = $user_profile->getField('id');
$name = $user_profile->getField('name');
$email = $user_profile->getField('email');
$profile_url = $user_profile->getField('link');
if (empty($profile_url))
$profile_url = https://www.facebook.com/$uid";
$profile_image_link = "https://graph.facebook.com/$uid/picture";


從字串初始化並檢查 token 是否過期:

v4:

use Facebook\FacebookSession;

$session = new FacebookSession( $token );
return $session->validate();

v5:

return $fb->getOAuth2Client()->debugToken( $token )->getIsValid() == true;


api 查詢架構:

v4:

function query($token, $method, $api, $array_mode = true) {
if (!empty($token)) {
$session = new FacebookSession($token);
                 if (!$session->validate())
return false;
$response = ( new FacebookRequest( $session, $method, $api ) )->execute();
if ($array_mode)
return $response->getGraphObject()->asArray();
return $response->getGraphObject();
}
return false;
}

v5:

function _node_query($token, $method, $api, $array_mode = true) {
if (!empty($token)) {
try {
if (!strcasecmp($method, 'POST'))
return $array_mode ? $this->fb->post($api, $token)->getGraphNode()->asArray() : $this->fb->post($api, $token)->getGraphNode();

return $array_mode ? $this->fb->get($api, $token)->getGraphNode()->asArray() : $this->fb->get($api, $token)->getGraphNode();
} catch (Exception $e) {
var_dump($e);
}
}
return false;
}

function _edge_query($token, $method, $api, $array_mode = false) {
if (!empty($token)) {
try {
if (!strcasecmp($method, 'POST'))
return $array_mode ? $this->fb->post($api, $token)->getGraphEdge()->asArray() : $this->fb->post($api, $token)->getGraphEdge();

return $array_mode ? $this->fb->get($api, $token)->getGraphEdge()->asArray() : $this->fb->get($api, $token)->getGraphEdge();
} catch (Exception $e) {
var_dump($e);
}
}
return false;
}


取得使用者權限清單:

v4:

query($token, 'GET', '/me/permissions');

v5:

_edge_query($token, 'GET', '/me/permissions');


查詢朋友清單:

v4:

function get_friend_installed_app_list($token, &$have_next_page, $page = 1, $item_per_page = 25) {
$have_next_page = false;
$data = $this->query($token, 'GET', '/me/friends?fields=installed&limit='.($item_per_page).'&offset='.(($page - 1) * $item_per_page));
if(isset($data['data']) && ($cnt = count($data['data'])) > 0) {
if (isset($data['paging']) && is_object($data['paging']) && property_exists($data['paging'], 'next')) {
if ($cnt >= $item_per_page)
$have_next_page = true;
}
$output = array();
foreach($data['data'] as $user)
if (is_object($user) && property_exists($user, 'id'))
array_push($output, $user->id);
return $output;
}
return false;

}

v5:

function get_friend_installed_app_list($token, &$have_next_page, $page = 1, $item_per_page = 25) {
$have_next_page = false;
$data = $this->_edge_query($token, 'GET', '/me/friends?fields=installed&limit='.($item_per_page).'&offset='.(($page - 1) * $item_per_page), false);
$items = $data->asArray();
if(is_array($items) && ($cnt = count($items)) > 0) {
$paging = $data->getMetaData();
if (isset($paging['paging']) && isset($paging['paging']['next']))
if ($cnt >= $item_per_page)
$have_next_page = true;
$output = array();
foreach($items as $user)
if (isset($user['id']))
array_push($output, $user['id']);
return $output;
}
return false;
}


收工!

2017年3月1日 星期三

一打一的世界

趁著連假,扛著一個小孩返鄉,體驗了走路、公車、捷運和高鐵。挑戰最大的是高鐵,在一個壅擠的座位上哄著小孩,也算是個磨練了。還好我不用選台鐵,那肯定是瘋了!

這陣子咀嚼著又似看透一些世俗話題,從黃毛小子、強說愁,總算累積了一點社會經歷,只是如同雷軍創業的初始心態:財務自由了,再來談創業、再來談不為錢做事的方向。很多事的起頭,都是有前提的,也認清自己不夠本去做哪些事。

返鄉看著兩老和周邊的考驗,漸漸浮現出這類現實的感觸,不再像學生時代的低機會成本,不再是年少輕狂自傲能扛著天,我想,漸漸地就像練武的人,什麼一次打十個,還是逃命吧 XD 一次打一個吧!

我想,我還是會積極地把握機會,但需要更沉得住氣、需要更多耐心和命運磨合磨合。

2017年2月24日 星期五

Captive portal 研究心得,以 iptables 實作

之前略知使用 Taipei Free 或 CHT Wi-Fi時,會彈跳出認證(登入)的網頁,但卻不知其所以然,直到最近研究了 Captive portal 的東西。一開始用 Captive portal 關鍵字找了一些 RFC 文件,殊不知那些都是煙霧彈 XD 如:
經過了一陣抓封包後,簡言之,透過網路管控,當符合以下情境時,各個 OS 內的網路偵測小程式,就會彈跳視窗:
  1. 用戶連上 router 時,可以進行 DNS lookup
  2. 未通過認證的用戶,無法透過 router 進行網路連線請求,如連上 facebook.com
  3. 通過認證的用戶,透過 router 可以正常使用網路
在各個 OS 內,都有一隻小程式時時關注著網路變化,當網路狀態改變且尚未有連外環境,這時程式會自動進行網路連線偵測,概念上就是去連各個 OS 自訂的一個網頁位置,看看能不能抓到東西,且東西是否正常。若不正常時,就會判斷網路是要認證的,接著就會彈跳出認證視窗。



至於 router 上的實作,簡單的說,就是透過防火牆機制:
  1. 防火牆設計各種狀態,例如未認證,已認證的 flag (mark)
  2. 當用戶完成認證時,透過 CGI 執行防火牆指令,給予標記 (mark)
  3. 允許已標記的 client 封包通行
至於完整的實作,可以參考:
以下是同事分別安裝 wifidog 跟 NoCatSplash 取出的防火牆規則,以 wifidog 產出的:

-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
-N WD_wlan0_AuthServs
-N WD_wlan0_Global
-N WD_wlan0_Internet
-N WD_wlan0_Known
-N WD_wlan0_Locked
-N WD_wlan0_Unknown
-N WD_wlan0_Validate
-A FORWARD -i wlan0 -j WD_wlan0_Internet
-A FORWARD -i eth0 -o wlan0 -m state --state RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -i wlan0 -o eth0 -j ACCEPT
-A WD_wlan0_AuthServs -d SERVER_IP/32 -j ACCEPT
-A WD_wlan0_Internet -m state --state INVALID -j DROP
-A WD_wlan0_Internet -o eth0 -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
-A WD_wlan0_Internet -j WD_wlan0_AuthServs
-A WD_wlan0_Internet -m mark --mark 0x254 -j WD_wlan0_Locked
-A WD_wlan0_Internet -j WD_wlan0_Global
-A WD_wlan0_Internet -m mark --mark 0x1 -j WD_wlan0_Validate
-A WD_wlan0_Internet -m mark --mark 0x2 -j WD_wlan0_Known
-A WD_wlan0_Internet -j WD_wlan0_Unknown
-A WD_wlan0_Known -j ACCEPT
-A WD_wlan0_Locked -j REJECT --reject-with icmp-port-unreachable
-A WD_wlan0_Unknown -p udp -m udp --dport 53 -j ACCEPT
-A WD_wlan0_Unknown -p tcp -m tcp --dport 53 -j ACCEPT
-A WD_wlan0_Unknown -p udp -m udp --dport 67 -j ACCEPT
-A WD_wlan0_Unknown -p tcp -m tcp --dport 67 -j ACCEPT
-A WD_wlan0_Unknown -j REJECT --reject-with icmp-port-unreachable
-A WD_wlan0_Validate -j ACCEPT


以 NoCatSplash 產出的 iptables 為筆記:

$ iptables -S
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
-N NoCat
-N NoCat_Inbound
-N NoCat_Ports
-A FORWARD -j NoCat
-A NoCat -j NoCat_Ports
-A NoCat -j NoCat_Inbound
-A NoCat -s SERVER_IP/24 -i wlan0 -m mark --mark 0x1 -j ACCEPT
-A NoCat -s SERVER_IP/24 -i wlan0 -m mark --mark 0x2 -j ACCEPT
-A NoCat -s SERVER_IP/24 -i wlan0 -m mark --mark 0x3 -j ACCEPT
-A NoCat -j DROP
-A NoCat_Ports -i wlan0 -p tcp -m tcp --dport 25 -m mark --mark 0x3 -j DROP
-A NoCat_Ports -i wlan0 -p udp -m udp --dport 25 -m mark --mark 0x3 -j DROP
$ iptables -t nat -S
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
-N NoCat_Capture
-N NoCat_NAT
-A PREROUTING -j NoCat_Capture
-A POSTROUTING -j NoCat_NAT
-A NoCat_Capture -p tcp -m mark --mark 0x4 -m tcp --dport 80 -j REDIRECT --to-ports 5280
-A NoCat_Capture -p tcp -m mark --mark 0x4 -m tcp --dport 443 -j REDIRECT --to-ports 5280
-A NoCat_NAT -s SERVER_IP/24 -o eth0 -m mark --mark 0x1 -j MASQUERADE
-A NoCat_NAT -s SERVER_IP/24 -o eth0 -m mark --mark 0x2 -j MASQUERADE
-A NoCat_NAT -s SERVER_IP/24 -o eth0 -m mark --mark 0x3 -j MASQUERADE


其他 iptabes 簡介: