2018年12月9日 星期日

[Linux] dev_appserver.py ERROR module.py:1652] The PHP runtime cannot be run with the "Memcache" PECL extension installed @ Ubuntu 16.04

在 Ubuntu server 上,首推 https://cloud.google.com/sdk/downloads#linux 安裝法,不要靠 apt 套件管理,不然會踩到很多麻煩:

$ curl https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-183.0.0-linux-x86_64.tar.gz | tar -xvf -
$ cd google-cloud-sdk
$ ./install.sh


裝完後,若是要運行 php-sdk 時,會踩到 php 環境問題,通常就是要指定 --php_executable_path 位置:

$ dev_appserver.py --enable_host_checking false --host 0.0.0.0 --port 8080  --php_executable_path /usr/bin/php-cgi5.6 app.yaml

只是執行時又會踩到缺套件等等的問題,裝到最後產生了 Memcache 套件問題,就要再把 Memcache 套件關閉即可:

$ php5.6 --ini
Configuration File (php.ini) Path: /etc/php/5.6/cli
Loaded Configuration File:         /etc/php/5.6/cli/php.ini
Scan for additional .ini files in: /etc/php/5.6/cli/conf.d
Additional .ini files parsed:      /etc/php/5.6/cli/conf.d/10-opcache.ini,
/etc/php/5.6/cli/conf.d/10-pdo.ini,
/etc/php/5.6/cli/conf.d/20-apcu.ini,
/etc/php/5.6/cli/conf.d/20-bcmath.ini,
/etc/php/5.6/cli/conf.d/20-calendar.ini,
/etc/php/5.6/cli/conf.d/20-ctype.ini,
/etc/php/5.6/cli/conf.d/20-exif.ini,
/etc/php/5.6/cli/conf.d/20-fileinfo.ini,
/etc/php/5.6/cli/conf.d/20-ftp.ini,
/etc/php/5.6/cli/conf.d/20-gettext.ini,
/etc/php/5.6/cli/conf.d/20-iconv.ini,
/etc/php/5.6/cli/conf.d/20-igbinary.ini,
/etc/php/5.6/cli/conf.d/20-json.ini,
/etc/php/5.6/cli/conf.d/20-memcache.ini,
/etc/php/5.6/cli/conf.d/20-msgpack.ini,
/etc/php/5.6/cli/conf.d/20-phar.ini,
/etc/php/5.6/cli/conf.d/20-posix.ini,
/etc/php/5.6/cli/conf.d/20-readline.ini,
/etc/php/5.6/cli/conf.d/20-shmop.ini,
/etc/php/5.6/cli/conf.d/20-sockets.ini,
/etc/php/5.6/cli/conf.d/20-sysvmsg.ini,
/etc/php/5.6/cli/conf.d/20-sysvsem.ini,
/etc/php/5.6/cli/conf.d/20-sysvshm.ini,
/etc/php/5.6/cli/conf.d/20-tokenizer.ini,
/etc/php/5.6/cli/conf.d/25-memcached.ini

$ sudo vim /etc/php/5.6/cli/conf.d/20-memcache.ini
; uncomment the next line to enable the module
;extension=memcache.so

$ sudo vim /etc/php/5.6/cli/conf.d/25-memcached.ini
; priority=25
;extension=memcached.so

2018年12月3日 星期一

Android 開發筆記 - Image Picker 與權限管理

在 Android 6.0 (API:23) 後,就算 AndroidManifest.xml 寫好要權限的部分,但實務上在進行時,依舊要主動要一次,讓用戶感受到真的要權限了:https://developer.android.com/training/permissions/requesting

做 Image Picker 時,若最終要讀取資料,那需要 android.permission.READ_EXTERNAL_STORAGE 權限,以下則是在 MainActivity 運行的範例:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
</manifest>


public class MainActivity extends AppCompatActivity {
private static final int REQUEST_SELECT_VIDEO = 1;
private static final int REQUEST_EXTERNAL_STORAGE = 2;
private static String[] PERMISSIONS_STORAGE = {
Manifest.permission.READ_EXTERNAL_STORAGE,
};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, PERMISSIONS_STORAGE, REQUEST_EXTERNAL_STORAGE);
} else {
request_pick();
}
}

void request_pick() {
final Uri mUri = android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
final Intent mIntent = new Intent(Intent.ACTION_PICK, mUri);
final PackageManager mPackageManager = getPackageManager();
List<ResolveInfo> list = mPackageManager.queryIntentActivities(mIntent, PackageManager.MATCH_DEFAULT_ONLY);
if (list.size() > 1) {
startActivityForResult(
Intent.createChooser(
new Intent(Intent.ACTION_PICK, mUri), "選取圖片"
),
REQUEST_SELECT_VIDEO
);
} else {
startActivityForResult(mIntent, REQUEST_SELECT_VIDEO);
}
}

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK) {
if (requestCode == REQUEST_SELECT_VIDEO) {
// ...
// 在處理檔案讀取時,缺少 android.permission.READ_EXTERNAL_STORAGE 會造成 IOException:
// open failed: EACCES (Permission denied)
// ...
}
}
}
}

2018年11月9日 星期五

macOS 開發筆記 - fatal error: 'stdio.h' file not found

在 macOS 搭配 CMake 編譯東西時,竟然會缺 stdio.h ,只好:

$ sudo xcode-select --install
$ open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg


收工

2018年10月22日 星期一

[PHP] Dashboard 開發筆記 - 透過 date 和 strtotime 產生日期變化

開發 Dashboard 時,需要一些時間變化,這時可以靠 strtotime 來幫忙轉,十分方便

$ cat /tmp/t.php
<?php

echo date("Y/m/d")."\n";
echo date("Y/m/d", strtotime(date("Y/m/d")." -1 day"))."\n";
echo date("Y/m/d", strtotime(date("Y/m/d")." -2 day"))."\n";
echo date("Y/m/d", strtotime(date("Y/m/d")." -7 day"))."\n";
echo date("Y/m/d", strtotime(date("Y/m/d")." -8 day"))."\n";
echo date("Y/m/d", strtotime(date("Y/m/d")." -30 day"))."\n";
echo date("Y/m/d", strtotime(date("Y/m/d")." -31 day"))."\n";
echo date("Y/m/d", strtotime(date("Y/m/d")." -60 day"))."\n";


$ php /tmp/t.php
2018/10/22
2018/10/21
2018/10/20
2018/10/15
2018/10/14
2018/09/22
2018/09/21
2018/08/23

[PHP] Dashboard 開發筆記 - 使用 Google Analytics 和 Google Adsense API 追蹤數據

AU_GroupByCountry

大概三個禮拜前已經做好了一些研究,一直偷懶沒寫點紀錄 :P 程式碼片段:

https://github.com/changyy/TrackingLibrary/tree/master/php/Codeigniter-3.1.9/application/libraries

取得 Google Analytics 數據 - Google_Service_Analytics 用法:

$this->load->library('GaQuery_lib', array(
'vendor/autoload.php' => 'vendor/autoload.php',
'access_token' => $access_token,
));

$ret = $output = $this->gaquery_lib->query(
$this->ga_profile_id,
'20180901', // $this->input->get_post('date_start'),
'20190930', // $this->input->get_post('date_end'),
'ga:users,ga:newUsers', // $this->input->get_post('metrics'),
[
'dimensions' => 'ga:countryIsoCode',
'sort' => '-ga:users'
], // $options
);


取得 Google Adsense 數據 - Google_Service_AdSense 用法:

$this->load->library('AdsenseQuery_lib', array(
'vendor/autoload.php' => 'vendor/autoload.php',
'access_token' => $access_token,
));

$ret = $output = $this->adsensequery_lib->getReport(
'20180901', // $this->input->get_post('date_start'),
'20190930', // $this->input->get_post('date_end'),
[
'dimension' => ['AD_UNIT_CODE'],
'metric' => [
'AD_REQUESTS',
'AD_REQUESTS_COVERAGE',
'AD_REQUESTS_CTR',
'CLICKS',
'COST_PER_CLICK',
'AD_REQUESTS_RPM',
'EARNINGS',
]
]
);


如此一來,光 GA 就可以組出不錯的數據,例如取出 60 天的數據,可以做出近30天與上個近30天的變化量,得知活躍用戶是否有對應成長,讓人決定要做哪個國家的客製化服務。

2018年10月20日 星期六

[SQL] 在 SQLite3 環境中,使用 Regular Expression @ Ubuntu 16.04

$ sudo apt-get install sqlite3-pcre
$ file /usr/lib/sqlite3/pcre.so
/usr/lib/sqlite3/pcre.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=0afc1236e89d4a8b99746f231049101172714c2c, stripped
$ sqlite3
SQLite version 3.11.0 2016-02-15 17:29:24
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
sqlite> SELECT date();
2018-10-19
sqlite> SELECT date() AS d WHERE d REGEXP '[0-9]';
Error: no such function: REGEXP
sqlite> .load /usr/lib/sqlite3/pcre.so
sqlite> SELECT date() AS d WHERE d REGEXP '[0-9]';
2018-10-19
sqlite> SELECT date() AS d WHERE d REGEXP '^[0-9]$';
sqlite> SELECT date() AS d WHERE d REGEXP '^[0-9]+$';
sqlite> SELECT date() AS d WHERE d REGEXP '^[0-9\-]+$';
2018-10-19

2018年10月11日 星期四

PHP 開發筆記 - 判斷瀏覽器語系機制

就 Javascript 來說,很方便:

var userLanguage = navigator.language || navigator.userLanguage;

而 server site 就靠 $_SERVER['HTTP_ACCEPT_LANGUAGE'] 啦,但他的格式還會有語系偏好比重,需要小小處理一番:

//
// $_SERVER['HTTP_ACCEPT_LANGUAGE'] == 'zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7';
//
function _detect_browser_language($_SERVER_VAR, $system_prefer = array( 'en' => 1, 'cn' => 1 )) {
if (isset($_SERVER_VAR['HTTP_ACCEPT_LANGUAGE'])) {
$langs = array();
foreach(explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']) as $entry) {
$t1 = explode(';', $entry);
$cnt = count($t1);
if ($cnt == 2) {
$t2 = explode('=', $t1[1]);
if (count($t2) == 2)
array_push($langs, array($t1[0], floatval($t2[1])));
else
array_push($langs, array($t1[0], 1.0));
} else if ($cnt == 1)
array_push($langs, array($t1[0], 1.0));
}
function lang_prefer_sort($a, $b) {
if( $a[1] == $b[1] ) return 0;
if( $a[1] > $b[1] ) return -1;
return 1;
}
usort($langs, 'lang_prefer_sort');
foreach($langs as $lang_info) {
if (is_array($lang_info)) {
if (isset($system_prefer[$lang_info[0]]))
return $lang_info[0];
$checker = explode('-', $lang_info[0]);
if (count($checker) == 2) {
if (isset($system_prefer[$checker[0]]))
return $checker[0];
else if ($checker[0] == 'zh')
return 'cn';
}
}
}
}
return 'en';
}

2018年9月18日 星期二

Android 開發筆記 - MenuItemCompat.getActionProvider return null

週末練練 Android 手感,試試 Cast SDK ,結果連最簡單的 CastButton 都弄不出來,一直碰到:

java.lang.IllegalArgumentException: menu item with ID ###### doesn't have a MediaRouteActionProvider.

跑去看了一下 CastButtonFactory.setUpMediaRouteButton 程式碼:

    public static MenuItem setUpMediaRouteButton(Context context, Menu menu, int i) {
        zzac.zzdn("Must be called from the main thread.");
        zzac.zzw(menu);
        CastContext sharedInstance = CastContext.getSharedInstance(context);
        MenuItem findItem = menu.findItem(i);
        if (findItem == null) {
            throw new IllegalArgumentException(String.format(Locale.ROOT, "menu doesn't contain a menu item whose ID is %d.", new Object[]{Integer.valueOf(i)}));
        }
        MediaRouteActionProvider mediaRouteActionProvider = (MediaRouteActionProvider) MenuItemCompat.getActionProvider(findItem);
        if (mediaRouteActionProvider == null) {
            throw new IllegalArgumentException(String.format(Locale.ROOT, "menu item with ID %d doesn't have a MediaRouteActionProvider.", new Object[]{Integer.valueOf(i)}));
        }
        mediaRouteActionProvider.setRouteSelector(sharedInstance.getMergedSelector());
        return findItem;
    }


才確認應該有什麼搞錯了,再追了一下,果真是 menu 的 xml 描述有問題,不小心把 app namespace 弄錯了,錯誤:

xmlns:app="http://schemas.android.com/tools"

正確:

xmlns:app="http://schemas.android.com/apk/res-auto"

就這樣耍廢了兩三個小時。

程式範例也才幾句話:

    @Override public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        getMenuInflater().inflate(R.menu.navigation, menu);
        CastButtonFactory.setUpMediaRouteButton(getApplicationContext(),
                menu,
                R.id.media_route_menu_item);
        return true;
    }


為了 debug :

    @Override public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);

        getMenuInflater().inflate(R.menu.navigation, menu);

        MenuItem mMenuItem = menu.findItem(R.id.media_route_menu_item);
        if (mMenuItem != null) {
            Log.v("onCreateOptionsMenu", "Found");
            MediaRouteActionProvider mediaRouteActionProvider =
                    (MediaRouteActionProvider) MenuItemCompat.getActionProvider(mMenuItem);

            if (mediaRouteActionProvider != null) {
                Log.v("MediaRouteActionProvider", "Found");
            } else {
                Log.v("MediaRouteActionProvider", "Not Found");
            }
        } else {
            Log.v("onCreateOptionsMenu", "Not Found");
        }
        return true;

2018年9月16日 星期日

刻意練習

刻意練習

幾個月前就買了這本書,當時在衝刺一些點子一直沒去翻。近幾個禮拜刻意讓自己緩下步,甚至週末都沒打開電腦寫任何程式。刻意練習,跟 "一萬小時法則" 有一點關係,最重要的是持續且找到有效精進自己的方式。

讀著這本書,想起自己學資訊的步調,就像寫 Blog 只是為了留下點什麼,都是刻意的足跡。套句高手的說法:看看 stack overflow 就行了啊,幹嘛花時間寫筆記?但也是這種刻意的筆記造就了一些小成果。

最近翻書的最大心得:不小心把目標設定太遠了,導致完全提不起勁去克服。但,太容易得手的目標,也是沒任何效果的。

回過頭來,只能繼續專注。生活存在有太多誘惑了。

2018年8月7日 星期二

攀談



跟幾位老友哈拉幾句,又再次發現空氣的新鮮。後來也才知道,大家進退之間,早已找好了位置。幾天前跟一位在 CIO 熱潮的前同事哈拉,順便細細地思考自己到底在追尋著什麼。

每一次的攀談都是不錯的躍進,可惜的是話上句點後,又瞬間跌落谷底 XD 什麼也沒改變。
老同事推薦翻翻李笑來的 "通往財富自由之路" ,重點落在複利的學習力、好好善用自己的長才去累積價值。

這幾天也認識了一位剛三十歲的女強人,先去泰國學語言幾個月,接著開公司、批深圳的貨來泰國賣,搞通泰國銷售流程、客服跟在地行銷,實在佩服。她不是業務出身,而完全就是同背景的IT系統整合,懂的善用時間不自己造輪(Shopify),懂的分析時勢在東南亞找到適合的位置,就這樣努力衝刺著,著實佩服。雖然跟老友哈拉這案例時,總會回歸到個人到底有多少資源可發揮,但我認為最重要仍是執行力啦。

隨時間流逝,會有越來越多的感觸吧,那可能已經不是後悔了。

2018年7月29日 星期日

[Apple] MacBook Pro 脫膜,在 Apple 台北101 Genius Bar 單日完成維修體驗 (Retina, 13-inch, Early 2015)

Apple 台北101 Apple 台北 101 維修人潮

我習慣買前一代的 Mac 產品,在 2016 年底買了這台 2015 年的產品(直到查詢脫膜才發現自己是 Early 2015 版),以為脫膜跟我無關了 XD 殊不知還是中獎,隨著使用時間開始出現跡象了,剛好這週得閒,就行動衝一發了 XD 不然有了家庭跟小孩,親自送修這種事真的太殺時間了!

整體上,我先打了電話到 Studio A ,得知評估脫膜還得把筆電放在那邊 7-14 天,當下就很三條線,立刻就改打電話去 Apple 台北101 ,看看直營店有沒有什麼差別。只是整個過程我去了三趟 台北 101 ,第一趟誤被台北101接電話的服務員給誤導,我在中午播電話過去,詢問可否現場排隊維修 Mac ,服務員說現在人很少可以試試,直到我到了現場才知道 Mac 維修是最夯的,通常只能預約,不然就排現場(11:00開門),排現場也只是看看有沒有預約取消的額度,並不確定一定可以修到。

隔天吃完早餐報備後,就出發去 101 了,約 10:30 到了,跟著陸客就莫名到了 Apple 台北101 的門口排隊了 (台北購物中心11:00才開門),大概排前五名,就這樣等個30分鐘就可入場維修!等到自己登記完維修後,才發現後排大概至少排了30人在等維修。

這大概就是 Apple Support app 上有維修預約服務,但實際上根本都搶不到只能排現場的現象,不知是不是暑假的關係?為了維修只能排現場了。

後來很順利得知有備料,交完筆電後,很有機會就在當日拿到!至於網路上有的人說可以先去評估維修,接著再等料來再拿去,這件事並不對的,脫膜換螢幕一律要把筆電放在店內等維修,有料無料都還跑會基本的 SOP 做整機檢驗。

所以,脫膜維修要有心理準備,沒筆電就是得停工一陣子。

心得:
  • 脫膜維修流程是會先判斷是否為人員問題,維修不管怎樣都得要與筆電分開一陣子,最慘的是 7-14天,如 Studio A 據說要把螢幕寄回去給原廠評估是否為人為。因此,去直營店好處好上不少,運氣好可以一早進去晚上就拿貨。
  • 下載 "Apple 支援" 或是網頁申請支援,都可以有專人電話服務(2分鐘內播來),但對於 Mac 維修還是走預約制度(且各間店頂多八天內可以選時段),結果台北夏天人多,預約根本選不到,等同只能排現場。排現場會先登記,後續簡訊通知是否輪到你

    Apple 支援 app Apple 台北101 簡訊服務
  • Apple 台北 101 ,建議有需要還是排一早前幾名,投資報酬率高,不然還是等預約

2018年7月25日 星期三

Google Home mini 體驗心得以及 Google Assistant app 下載方式

Google Home mini with Chromecast

由於公司年初就決定要做音控,我自己也練習過 Alexa skill 的開發,但一直偷懶沒去測試 Actions on Google,只好懲罰一下,買個 Google home mini 了 (誤)

之前買過 AIY Voice Kit (反而比成品貴一倍),體驗音控服務的開發門檻,像是無間斷辨識關鍵字、語音辨識、個人助理服務,當時面對的僅只是一個維度(聲音),殊不知跟 Chromecast 搭配起來,就更威猛了,多了影像維度,光聲音+影像兩個維度就覺得開始看不到車尾燈了,更別說還搭配了 Search Service、個人資料、甚至 Youtube service 這些維度(常用的大概還會用 google calendar 等)

很佩服 Google Home mini 在收到 Hey, Google / OK, Google 時,會自動把 Chromecast 靜音,這 UX 已經加很多分了,更別說播放一些關鍵字時,Youtube 沒把握時,乾脆在 Chromecast 顯示選擇題!這大概就加分加到爆了 XD

令人想起 HTML 上一個 button click 設計了陰影、動畫、聲音來增加 UX 體驗,手機上則是震動等觸感,而此時看到 Voice Control 的 Google Home mini 跟 Chromecast 搭配,真的有那種 1+1 > 2 的體驗,分開使用是完全沒法體會的。

其他常見問題:

  • iPhone 要記得先切手機語系到英文,在用 Google Home app 設定 Google Home mini ,不然會一直踩到 kGSKErrorDomain 錯誤 0 或 -83902
  • iOS Device 想要下載 Google Assistant app,只需把現有的帳號切換到 US 地區即可n搜尋的到並可以下載
  • Android Device 想要下載 Google Assistant app 只能靠 VPN 切換過去下載,另外要留意 Android 裝置有 OS 版本要求跟記憶體要求(我就是卡在記憶體沒 1.4GB 啦)
  • 據說 Google Home 在 2018 年底要推出繁體辨識了?此時若要先買只能靠露天或蝦皮,有的會附服務費發票,價錢落在 1300-1500 之間(不含運),而 AIY Voice Kit 則是貴太多,包含 Pi 3 板子、Voice Kit 盒子等等,隨便弄弄都要破 2500 吧

2018年7月1日 星期日

[NAS] Private IP Server 與 Let's Encrypt Wildcard SSL 憑證 / HTTPS 服務

家裡擺了一台 Synology NAS 躲在防火牆後面,一直都是用 private ip 在服務的,以前 Let's Encrypt 有免費的 SSL 憑證服務,但驗證方式需要讓對方連到 server ,一直無法在 private ip server 啟用,可惜了點!然而,Let's Encrypt 開放 Wildcard SSL 憑證服務,採用 DNS TXT Record 驗證,這時就可以上場了!

我的 NAS 是幾年前買的 Synology DS216play ,他的 CPU 挺麻煩的,讓我額外安裝軟體都很不便利,原先想採用 certbot 來做事,安裝了 python3、pip 和 virtualenv 後,最後還是卡關,卡在安裝軟體還需要 compiler 來編譯,擺爛三個月後,我改用了 https://github.com/Neilpang/acme.sh

acme.sh 的使用很方便:

$ wget https://raw.githubusercontent.com/Neilpang/acme.sh/master/acme.sh
$ chmod a+x acme.sh
$ ~/acme.sh --issue -d *.changyy.org --dns --yes-I-know-dns-manual-mode-enough-go-ahead-please


如此後續再添加 TXT record 後,在執行

$ ~/acme.sh --renew -d *.changyy.org --dns --yes-I-know-dns-manual-mode-enough-go-ahead-please

就搞定了!剩下的步驟只是覆蓋憑證檔案跟重啟 web server :

$ grep certificate /etc/nginx/nginx.conf
    ssl_certificate           /usr/syno/etc/certificate/system/default/fullchain.pem;
    ssl_certificate_key       /usr/syno/etc/certificate/system/default/privkey.pem;
$ ls /usr/syno/etc/certificate/system/default/
cert.pem  chain.pem  fullchain.pem  privkey.pem


最後,我把 DNS 擺在 cloudflare 託管,還可以用 API 去處理 DNS TXT Record 更新,連續動作如下:

$ ls ~/.acme.sh/
$ wget https://raw.githubusercontent.com/Neilpang/acme.sh/master/dnsapi/dns_cf.sh -O ~/.acme.sh/dns_cf.sh
$ chmod 755 ~/.acme.sh/dns_cf.sh
$ CF_Key=YOUR_API_KEY_AT_PROFILE CF_Email=YOUR_CLOUDFLARE_EMAIL_ACCOUNT ~/acme.sh --issue --dns dns_cf -d *.changyy.org
$ sudo cp ~/.acme.sh/\*.changyy.org/\*.changyy.org.cer /usr/syno/etc/certificate/system/default/cert.pem
$ sudo cp ~/.acme.sh/\*.changyy.org/\*.changyy.org.key /usr/syno/etc/certificate/system/default/privkey.pem
$ sudo cp ~/.acme.sh/\*.changyy.org/ca.cer /usr/syno/etc/certificate/system/default/chain.pem
$ sudo cp ~/.acme.sh/\*.changyy.org/fullchain.cer /usr/syno/etc/certificate/system/default/fullchain.pem
$ sudo synoservicecfg --restart nginx


如此一來,就可以安排 crontab 用 root 每月跑一次了,另一個好處是可以在 NAS 執行完,再把憑證丟到一些 VPS 維運的機器上

# cat /root/lets-ssl-renew.sh
#!/bin/sh
CF_Key=YOUR_API_KEY_AT_PROFILE CF_Email=YOUR_CLOUDFLARE_EMAIL_ACCOUNT /root/acme.sh --issue --dns dns_cf -d *.changyy.org  && cp /root/.acme.sh/\*.changyy.org/\*.changyy.org.cer /usr/syno/etc/certificate/system/default/cert.pem && cp /root/.acme.sh/\*.changyy.org/\*.changyy.org.key /usr/syno/etc/certificate/system/default/privkey.pem && cp /root/.acme.sh/\*.changyy.org/ca.cer /usr/syno/etc/certificate/system/default/chain.pem && cp /root/.acme.sh/\*.changyy.org/fullchain.cer /usr/syno/etc/certificate/system/default/fullchain.pem && synoservicecfg --restart nginx && echo "SSL-Renew-Done" > /root/ssl-renew.log || echo "SSL-Renew-Failed" > /root/ssl-renew.log
echo date >> /root/ssl-renew.log

2018年6月27日 星期三

親愛的奇利,謝謝你來到我們家

20090124 - 撒嬌

幾天前在辦公室得知奇利過逝,遲疑著幾分鐘後,就請個特休搭高鐵返鄉。一路上淚也止不下來,沒想到早上加油時送的面紙派上了用場。和奇利的緣分近 13 年吧!在我們家處境最糟的時候,加入的成員。

還記得剛來我們家時,傲氣的奇利還不准人摸他的頭,每次要摸就立刻轉身,隨著彼此的生活步調同步著,我教會奇利如何上下樓梯,奇利甚至就學會了不在家裡亂尿尿,忍不住時就在廁所方便,實在貼心。

回顧起來,奇利過得也不錯,只有一開始一兩年住在籠子裡,後來家裡環境比較乾淨後,就不在需要籠子,可以隨處挑喜歡的地方睡覺,甚至跟我們一起吹冷氣,接著陪著我們到處輕旅行,追過每一年的燈會、國際上知名的黃色小鴨以及不少台灣老少咸宜的景點,或是來北部待過我租的每一間房間,這種情誼不是寵物兩字可道盡,只有家人可以詮釋情誼。

近一年奇利健康老化,來台北時,不知是不是長途關係,走著走著就倒下了!而後才知道,他的生命到了末曲,老化帶來的心臟病,心臟瓣膜脫垂會讓他喘,吸不到空氣,開始吃藥、開始緩慢的步調。

回顧起來,我們是不是用著藥物強制延續著緣分?安樂死真的是個課題。但人竟然可以這樣決定別人的生命?
定期進入全氧室能得到紓解、每天早晚的藥物控制可以穩定作息,卻都換不回那可以快樂奔跑的奇利。

喜歡那爽朗的笑容,這樣癡癡的過生活。
非常感謝和奇利的緣分。

他可能只是你的一個寵物,但你卻是他的全部。
越多的互動,真的越難釋懷,只能朝著,奇利,你終於不用在吃藥了,可以開心自在的到處散步

謝謝你來到我們家


回矇一笑

2018年6月2日 星期六

[Python] Line Chatbot 開發筆記 - Echo Service @ Ubuntu 18.04

LineChatbot01

入口:https://developers.line.me/en/
文件:https://developers.line.me/en/docs/messaging-api/overview/

登入開發者後,先來個 Add new provider ,此例為 StudyLineChatbot ,接著選擇 Messaging API,填寫完基本資料後,再按幾下就創建了

其中,重要的資訊:
  • Channel secret
  • Channel access token (long-lived)
而 Channel access token (long-lived) 預設為空,要按個 Issue 來產生一組。

接著,Webhook URL 要填寫對接的 API 位置,該 API 必須提供 https 服務,另外再把 Auto-reply message 關閉。如此一來就完成 Line Chatbot 的設定。

接著談談 Webhook URL 的實作方式,此例用 Python 的 Flask 和 line-bot-sdk-python 完工,簡短程式 hello.py 如下(其實就是 https://github.com/line/line-bot-sdk-python README 範例程式,多增加一點輸出訊息):

from flask import Flask, request, abort

from linebot import (
    LineBotApi, WebhookHandler
)
from linebot.exceptions import (
    InvalidSignatureError
)
from linebot.models import (
    MessageEvent, TextMessage, TextSendMessage,
)

app = Flask(__name__)

line_bot_api = LineBotApi('YOUR_LINE_CHATBOT Channel access token (long-lived)')
handler = WebhookHandler('YOUR_LINE_CHATBOT Channel secret')

@app.route("/", methods=['POST'])
def callback():
    print("[INFO] Get request:")
    print(request.__dict__)
    print()

    # get X-Line-Signature header value
    signature = request.headers['X-Line-Signature']
    # get request body as text
    body = request.get_data(as_text=True)
    print("[INFO] Request Body: ---\n"+body+"\n---\n")

    # handle webhook body
    try:
        events = handler.handle(body, signature)
    except InvalidSignatureError:
        abort(400)

    print("[INFO] Handle Events \n")
    for event in events:
        if event is None:
            continue
        if not isinstance(event, MessageEvent):
            continue
        if not isinstance(event.message, TextMessage):
            continue
        line_bot_api.reply_message(
            event.reply_token,
            TextSendMessage(text=event.message.text)
        )
    return '{}'

@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    line_bot_api.reply_message(
        event.reply_token,
        TextSendMessage(text=event.message.text)
    )

if __name__ == "__main__":
    app.run(host='0.0.0.0', port=8080)


然後,選擇在 Ubuntu server 上,透過 nginx 跟 uWSGI 設定。

Nginx:

location / {

try_files $uri @chatbot;
}

location @chatbot {
include uwsgi_params;
uwsgi_pass unix:/tmp/chatbot-uwsgi.sock;
}


uWSGI:

$ vim /etc/systemd/system/chatbot.uwsgi.service
[Unit]
Description=uWSGI Emperor
After=syslog.target

[Service]
ExecStart=/usr/local/bin/uwsgi --ini /path/project/chatbot-uwsgi.ini
RuntimeDirectory=uwsgi
Restart=always
KillSignal=SIGQUIT
Type=notify
StandardError=syslog
NotifyAccess=all

[Install]
WantedBy=multi-user.target

$ vim /path/project/chatbot-uwsgi.ini
[uwsgi]
base = /path/project/

app = hello
module = %(app)
home = %(base)/venv
pythonpath = %(base)
callable = app

socket = /tmp/%n.sock
chmod-socket = 666
logto = /var/log/uwsgi/%n.log


後續都統一靠以下招數更新:

$ sudo systemctl restart chatbot.uwsgi

以及 hello.py 輸出的資料和運行結果,都可以在 /var/log/uwsgi/chatbot-uwsgi.log 找到:

[INFO] Get request:
{'environ': {'QUERY_STRING': '', 'REQUEST_METHOD': 'POST', 'CONTENT_TYPE': 'application/json;charset=UTF-8', 'CONTENT_LENGTH': '237', 'REQUEST_URI': '/', 'PATH_INFO': '/', 'DOCUMENT_ROOT': '/etc/nginx/html', 'SERVER_PROTOCOL': 'HTTP/1.1', 'REQUEST_SCHEME': 'https', 'HTTPS': 'on', 'REMOTE_ADDR': '###.###.###.###', 'REMOTE_PORT': '34994', 'SERVER_PORT': '443', 'SERVER_NAME': 'chatbot.example.com', 'HTTP_X_LINE_SIGNATURE': '#####################', 'HTTP_CONTENT_TYPE': 'application/json;charset=UTF-8', 'HTTP_CONTENT_LENGTH': '237', 'HTTP_HOST': 'chatbot.example.com', 'HTTP_ACCEPT': '*/*', 'HTTP_USER_AGENT': 'LineBotWebhook/1.0', 'wsgi.input': <uwsgi._Input object at 0x7f0d8a4477c8>, 'wsgi.file_wrapper': <built-in function uwsgi_sendfile>, 'wsgi.version': (1, 0), 'wsgi.errors': <_io.TextIOWrapper name=2 mode='w' encoding='UTF-8'>, 'wsgi.run_once': False, 'wsgi.multithread': False, 'wsgi.multiprocess': False, 'wsgi.url_scheme': 'https', 'uwsgi.version': b'2.0.17', 'uwsgi.node': b'###########', 'werkzeug.request': <Request 'https://chatbot.example.com/' [POST]>}, 'shallow': False, 'view_args': {}, 'url_rule': <Rule '/' (POST, OPTIONS) -> callback>, 'url': 'https://chatbot.example.com/'}

[INFO] Request Body: ---
{"events":[{"type":"message","replyToken":"###############","source":{"userId":"##############","type":"user"},"timestamp":###########,"message":{"type":"text","id":"###########","text":"HelloWorld"}}]}
---

2018年5月14日 星期一

iOS App 開發筆記 - 關於 case sensitive 檔案系統與 CocoaPods 和 Ruby/Gems 的處理

某天重灌 macOS 時,把主硬碟弄成 case sensitive ,想說自己都很熟悉 linux server 的行為,殊不知替自己埋了很多坑。首先,如果用 CocoaPods 維護套件時,別人的程式碼不見得會依照此規範,同理在 Ruby/Gems 更是如此。

解法?先切一個 partition 不區分大小寫吧!在把 Xcode build code 的暫存區也移至該 partition 來解!

未來還是推薦用 case insensitive 的 partition 吧 Orz 想搞龜毛還是用另一區,千萬別要跟青春過意不去啊。

將 Xcode build 臨時區移走:

$ mv ~/Library/Developer/Xcode/DerivedData/ ~/Library/Developer/Xcode/DerivedData-bak/
$ mkdir /Volumes/Data/XcodeDerivedData
$ ln -s /Volumes/Data/XcodeDerivedData ~/Library/Developer/Xcode/DerivedData


如此一來,關於 Xcode 與 CocoaPods 的大小寫問題應當可以解了,但...如果 build code 過程還有搭配 ruby script 做事,很抱歉,請自己改 ruby 了 Orz 例如:

require 'CFPropertyList'

很抱歉,在 Ruby 的 CFPropertyList 套件中,他實際是小寫的 Orz 請看:

https://github.com/ckruse/CFPropertyList/tree/master/lib

最重要的,這類還可能搞壞 gem 的管理狀態,這時解法就有點痛苦了,絕對不是三兩句就解掉的。

2018年5月9日 星期三

Alexa Skill 開發筆記 - 使用 AWS Lambda 與 OAUTH2 帳號連結

公司的產品是 WiFi Display 設備,結合智慧音箱後,透過音控請裝置執行動作,如播放影片。在此就順便紀錄這些開發過程。此處不會提及 OATUH2 開發項目。

首先,Alexa Skill 是個滿好玩的點子,說穿了就像 Apple TV 可以安裝 Apps 的概念,他是個市集,可以讓開發者提供更多智慧音箱的技能擴充,大約是 2015 年開始的。上頭也可以有簡單的變現機制,可以參考:Alexa開發者也可以賺錢了!看亞馬遜怎麼開啟語音應用的全新獲利模式

首先,沒有 Alexa device 也可以開發,善用模擬器即可。開發流程:

  1. 建立 Alexa Developer 帳號(過程就是建立 Amazon 帳號)
  2. 建立 AWS 帳號(綁定信用卡,有免費額度可善用)
  3. 使用模擬器服務 - https://echosim.io/


01-skill01

02-skill02

先在 Alexa Developer 先建立一個 Skill project,此例是 Video ,接著就要填寫 AWS Lambda 的位置,就切換到 AWS Lambda 創建流程,而 Alexa Skill 預設是提供 English (US) 的語系,參照 To create a Lambda function 第三步有強調 AWS Region 的部分:
Make sure you’ve selected the N.Virginia for English (US) skills or the EU (Ireland) region for English (UK) and German skills. The region is displayed in the upper right corner. Providing your Lambda function in the correct region prevents latency issues.
就要在 N.Virginia 創建 AWS Lambda function,不然亂建立根本無法互動。建立完 AWS Lambda function 後,替他增加 Alexa Skill Kit 和 Alexa Smart Home ,並且設定它的 Skill ID,並且把對應的程式碼上傳,且要記得發布出去!

03-aws01

04-aws02

05-aws03

如此一來,此 Skill 作者基本上已經可以測試了,而測試的條件包括要有一個 Alexa 音控裝置,這時就要靠模擬器,可以先到 https://echosim.io/ 登入 Amazon 帳號,即可獲得一台虛擬器。

15-simulator

接著,若你人在美國或是擁有美國的 iTunes / Google play 帳號可以下載得到 Amazon Alexa app ,那就直接用,不然只好跟我一樣用用網頁版: https://alexa.amazon.com/spa/index.html ,只要有先把 Amazon 帳號配對過 Alexa 音箱就可以正常切換到 Skill 頁面,點選右上角的 Your skill 即可切換到 Dev skill 來查看,並把自己開發的 skill 給 enable,過程就會觸發 OAUTH2 服務帳號的綁定並授權 Skill 服務使用等等,後續的過程先挑選哪個硬體裝置要被智慧音箱控制(AWS Lambda 要實作 device discovery 等機制),接著再挑哪個智慧音箱來搭配,整個過程用瀏覽器開發工具查看到 api response:

  • https://alexa.amazon.com/api/phoenix/discovery
    • 得知哪些硬體裝置要被控制
  • https://alexa.amazon.com/api/devices-v2/device
    • 得知你的帳號有哪些智慧音箱裝置


06-skill03

07-skill04

08-skill05

09-skill06

10-skill07

11-skill08

12-skill09

最後,自己測試完後,可以再開啟 Beta Test 機制,邀請其他人來測試。而要開啟 Beta Test 只需要把資料填一填,並處於 ready for submission 即可(送審前一步),關鍵的地方就是補一補 icon、描述、甚至一些網址等等,若只要一直內測就可以先亂填

13-skill10

14-skill11

最後,有問題真的要好好看文件 XD 而 AWS Lambda 則可以善用 Cloud Watch 看 logs ,以此確保真的有連線

2018年5月7日 星期一

處理 Google Cloud Messaging for Android (GCM) token 重複問題 (canonical_ids)

事情是這樣的,拖了很久才開始處理這段 XD 拖到連 GCM 都要下線了:

As of April 10, 2018, Google has deprecated GCM. The GCM server and client APIs are deprecated and will be removed as soon as April 11, 2019. Migrate GCM apps to Firebase Cloud Messaging (FCM), which inherits the reliable and scalable GCM infrastructure, plus many new features. See the migration guide to learn more.

為何要處理?主因是一隻 android mobile device 很有可能因為隱私狀態改變、重新安裝軟體後,導致 GCM token 變更,如果當年沒有設計 global uuid 來辨識移動裝置的話(像是綁個人帳號、用 wifi mac_address 等),那 token 肯定會收到一卡車多,這時要發送訊息時,就可能出現多個 token 對應到同一位使用者,造成用戶被 Push  轟炸 (這些在 Firebase 設計理念上都有改善了,例如可以單純對全部 android user 發訊息等等),特別是 QA 常常安裝反安裝程式...

原先我們依賴著 wifi mac_address ,但在某版 Android 發現他是可變的 Orz (忘了是隱私提升還是某OEM廠的bug) 對於維護 GCM token ,就只剩下一條路:試著對他發送訊息,檢查回應是不是有變化。

連續動作:

$check_failed_db_ids = array();
$tokens = array(
array( 'id' => 'db record id', 'token' => 'GCM token'),
// ...
);

$payload = array(
'registration_ids' => array(),
'dry_run' => true,
'data' => array(
'body' => 'test'
),
);

while(count($tokens) > 0) {
//
// Step 1: init
//
$payload['registration_ids'] = array();
$current_task = array_splice($tokens, 0, 900);
foreach($current_task as $items) {
array_push($payload['registration_ids'], $items['token']);
}

//
// Step 2: call gcm api
//
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://android.googleapis.com/gcm/send");
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
"Authorization: key=".$api_key,
'Content-Type: application/json'
));
curl_setopt($ch, CURLOPT_POST , true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
//curl_setopt($ch, CURLOPT_VERBOSE, true);
$ret = curl_exec($ch);
curl_close($ch);

$ret_json = @json_decode($ret, true);
if (isset($ret_json['results'])) {
$echo_cnt = count($ret_json['results']);
for ($i=0;$i<$echo_cnt; ++$i) {
// 通常有 error 都可以考慮去掉了;當有 registration_id 則是代表已轉換 token
// 完整範例跟解釋:https://developers.google.com/cloud-messaging/http#example-responses
if (isset($ret_json['results'][$i]['error']) || isset($ret_json['results'][$i]['registration_id'])) {
array_push($check_failed_db_ids, $i);

}
}
}

//
// Step 3: update db record & reset $check_failed_db_ids
//
// ...

}

2018年4月20日 星期五

[SEO] Google search console 與使用 wget 建立 sitemap @ macOS 10.13.14

最近公司換了網域,導流量過程中需要安置個 sitemap 來增加曝光,就先查了一下 Google 牌的定義:

https://support.google.com/webmasters/answer/183668?hl=zh-Hant

不錯不錯,可以提交純文字,那就搭配個 wget 吧!

$ time wget --spider --recursive --no-verbose --output-file=log.txt https://example.com/
...
real 87m25.965s
user 0m1.836s
sys 0m4.081s


真久,接著再靠 grep 跟 awk 即可:

$ grep -op "URL:http[s]://\(.*\) " ~/log.txt | awk '{print(substr($1,5));}' > sitemap.txt

搞定!

2018年4月3日 星期二

[PHP] 微信開放平台開發筆記 - WeChat Web 應用與 OAuth2 client

今年才感到 Wechat app 崛起,得知用戶已破10億,甚至微信支付交易筆數早已超車支付寶1.5倍,已乃覺三十里 Orz 想要用境外身份申請,才發現很多都辦不成,像是境外要用公司身份才行,甚至騰訊公開平台的管理者都還得綁有中國銀行卡(金融卡)才搞得定 <囧> 多虧對岸同事的幫忙,可以小試身手一下。

騰訊公開平台的 OAuth2 文件: https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419316505&token=&lang=zh_CN

需要的留意的是騰訊公開平台的 Web 應用只允許填入一個 hostname/domain 作為 callback handler ,所以,也有人想得很快,透過在統一地方處理完畢後,在拋去其他地方,如:https://github.com/HADB/GetWeixinCode

在此利用一樣的架構,不過是在 backend 處理 XD 主要是服務常分成 production/alpha/dev site,就把它統一建構在 Production site ,但 Dev site 要登入時,跳去 production site 處理後,再把資料回拋到 dev site(當然也可以申請多個 WeChat Web 應用,每個 site 擁有自己的 Web app)

整個過程都算順利,唯一不太順的地方是 OAuth state 這個參數的使用,原先埋了要跳去其他地方的 callback url 或其他資訊,但測了幾輪發現 state 內若有一些敏感資訊(&)時,回來都會出錯,於是就多加個 base64_encode 來解了(也可以考慮把必要的資料儲存在 cookie)

發動處:

<?php

$init_url = 'https://open.weixin.qq.com/connect/qrconnect?'. http_build_query( array(
        'scope' => 'snsapi_login,snsapi_userinfo',
        'response_type' => 'code',
        'redirect_uri' => 'https://auth.my-domain/wechat/callback',
        'state' => base64_encode( http_build_query(array(
                'give_me_code' => 1,
                'callback' => 'https://service.my-domain/wechat/handle-code',
        )) ),
        'appid' => $weixin_app_id,
));


負責處理 WeChat OAuth2 回傳資料(code):https://auth.my-domain/wechat/callback

<?php

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://api.weixin.qq.com/sns/oauth2/access_token');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query( array(
'appid' => $weixin_app_id,
'secret' => $weixin_app_secret,
'code' => $code,
'grant_type' => 'authorization_code',
)));
$ret = @json_decode(curl_exec($ch), true);
             
$state = $_GET['state'];
$state_param = array();
if (!empty($state)) {
$state = @base64_decode($state);
parse_str($state, $state_param);
}

isset($state_param['callback']) && (
preg_match('#http[s]://(.*?)\.my-domain/#i', $state_param['callback'], $match)
) ) {
if (!empty($_GET['code'])) {
if (isset($state_param['give_me_code']) && $state_param['give_me_code'] == '1')
header('Location: '.$state_param['callback'].'?'.http_build_query(array('code' => $_GET['code'])) );
// ...
} else {
header('Location: '.$state_param['callback'].'?'.http_build_query(array('error' => 'no code')) );
}
return;
}


處理 code:

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://api.weixin.qq.com/sns/oauth2/access_token');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query( array(
'appid' => $weixin_app_id,
'secret' => $weixin_app_secret,
'code' => $ret_code,
'grant_type' => 'authorization_code',
)));

$ret = @json_decode(curl_exec($ch), true);

if (isset($ret['access_token']) && isset($ret['refresh_token']) && isset($ret['openid']) && isset($ret['unionid'])) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://api.weixin.qq.com/sns/userinfo');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query( array(
'access_token' => $ret['access_token'],
'openid' => $ret['openid'],
)));
$profile_ret = @json_decode(curl_exec($ch), true);
}

// $profile_ret['nickname']
// $profile_ret['headimgurl']


原先在 callback handler 上,順便做完 code 檢驗,但後來覺得這樣要傳出去的資料太多(且感到不安),所以還是統一把 code 拋出去,缺點是其他服務就必須都紀錄 WeChat Web 應用的資料(app_id/secret_key)等等,但可以避免受到 DNS 攻擊而把用戶的 access_token 給了出去。

[Android] 使用 Chrome Browser - Web Inspector 查看 App WebView 行為

之前用了一陣子,然後又忘了,還是寫一下筆記:

  1. 要把 Android app 內使用的 Webview 開啟 setWebContentsDebuggingEnabled
    • WebView.setWebContentsDebuggingEnabled(true);
  2. Android phone 也要開啟 developer mode 和 adb debug
  3. 使用 Chrome browser -> 開發人員選項 -> more tools -> remote devices

順利的話,就會看到手機彈跳出一些視窗,按一按授權就看在 chrome browser 上追蹤 App 內 webview 的行為,像是追蹤其他國家內的網頁資源有被有被 ban 掉 XD

請直接參考:https://developers.google.com/web/tools/chrome-devtools/remote-debugging/?hl=zh-tw

未來可以規劃 release apk 不開啟 setWebContentsDebuggingEnabled ,而 dev apk 再開啟即可。

2018年4月1日 星期日

[Linux] Let's Encrypt 與 Wildcard 憑證,申請免費 SSL 憑證/HTTPS服務 @ Ubuntu 14.04

*.changyy.org

之前看強者同事已經幫公司網域申請完畢了,我才想到自己的網站一直沒打通 XD 趁個週日練一下吧!

看一下 Let's encrypt 網站簡介:https://letsencrypt.org/docs/client-options/
建議用 Certbot !

挑了一下 Nginx 與 Ubuntu server:

$ sudo apt-get install software-properties-common
$ sudo add-apt-repository ppa:certbot/certbot
$ sudo apt-get update
$ sudo apt-get install python-certbot-nginx


然後就只要指道幾令即可:

$ sudo certbot --manual certonly  -d *.changyy.org --server https://acme-v02.api.letsencrypt.org/directory

過程會要求你添加 DNS TXT record,添加完再按 Enter 驗證。

最後產出:

/etc/letsencrypt/live/changyy.org/fullchain.pem
/etc/letsencrypt/live/changyy.org/privkey.pem


來個 /etc/nginx/conf.d/ssl.changyy.org.conf

server {
    listen       80;
    listen  443 ssl;
    ssl_certificate /etc/letsencrypt/live/changyy.org/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/changyy.org/privkey.pem;

    server_name  ssl.changyy.org;

    access_log  /var/log/nginx/ssl.changyy.org-access.log  main;

    location / {
        root   /usr/share/nginx/html;
        index  index.php index.html index.htm;
    }
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}


收工!若是使用 Cloudflare 託管 DNS 的話,也可以透過 Certbot 一口氣都做完事喔:

Welcome to certbot-dns-cloudflare’s documentation!

另外,對我而言,這個服務最想用的就是 private ip server 啦!像是 NAS 等等的,只要透過手動設定即可搞定!

2018年3月10日 星期六

[Python] Google Voice Kit 與 Raspberry Pi 3

AIY4

上一次 Pi 1 開機已經想不起來了,大概是 2013 年的事吧,那年在研究 Raspbmc / Airplay 吧,大概是配置的 SDCard 太慢了,再加上已經長期投資 Linode 後,就漸漸把機器擺進防潮箱了,最近則是因為智慧喇吧的興起,想挑一款產品試試,想著想著,就想到去年就曾看到 Google Voice Kit ,而當初買不下去是因為美國才賣 10 美金,但台灣賣超過 30 美金,更別說美國那邊可是 35 美金含 Pi 3 的方案啊 XD

後來想通後,把多出來的價碼當作代購吧 Orz 何必為了一點還可以承擔的錢,延遲或放棄學習的機會?時間可是不等人的!總之,就這樣噴了將近快台幣 3000 吧,一張近似裸裝 Pi 3 (13XX) + Google Voice Kit (900) + microSD (300),各別都還要加上運費和轉帳費。(若單純對智慧音箱有興趣的,建議可以直接買 Google Home mini ,在露天代購不用兩千,甚至接近一千五,跟定價 49.99 美金非常接近)

AIY3

安裝硬件跟紙盒還滿快的,15 分鐘內應當可以搞定,接下來是軟體與線上服務的搭配,開通 GOOGLE ASSISTANT API 及 OAuth 認證後,就可以執行非常簡單的 demo 機制。

寫到這,的確可以穩穩體驗一下,但突然很反骨的想自己試試一些“中文”,需要搞定的項目為:
  • 語音輸入
  • 語音播放
  • 語音辨識
  • 語音合成(Text to Speech)
整個流程就是善用資源,哪邊有 code 要翻出來拼裝,像是語音輸入跟播放就用 Google 寫好的 aiy.audio,而語音辨識則是 SpeechRecognition ,語音合成則是 gTTS。比較特別的是語音播放是用 wav 格式,而 gTTS 的語音合成產出是 mp3 格式,所以再透過 pydub 轉成 wav (底層是 ffmpeg)

其中找資料的過程,主要是從 ~/Desktop/check_audio.desktop 進而研究 AIY-projects-python/checkpoints/check_audio.py 的寫法,找出底層實作的方式(command line!):

AIY-projects-python/src/aiy/audio.py
aiy._drivers._player
aiy._drivers._recorder
aiy._drivers._tts

AIY-projects-python/src/aiy/_drivers/_recorder.py
$ arecord --version
arecord: version 1.1.3 by Jaroslav Kysela <perex@perex.cz>

AIY-projects-python/src/aiy/_drivers/_player.py
$ aplay --version
aplay: version 1.1.3 by Jaroslav Kysela <perex@perex.cz>

AIY-projects-python/src/aiy/_drivers/_tts.py
$ pico2wave --usage
Usage: pico2wave [-?] [-w|--wave=filename.wav] [-l|--lang=lang] [-?|--help] [--usage] <words>


如果有興趣想追 GOOGLE ASSISTANT API 的實作,也可以多看看這些:

~/AIY-projects-python/src/examples/voice/assistant_library_with_button_demo.py
~/AIY-projects-python/src/examples/voice/assistant_library_demo.py
~/AIY-projects-python/src/aiy/assistant/library.py


很快就會追到 google.assistant.library,接著發現無法欣賞他如何實作 https://github.com/googlesamples/assistant-sdk-python/issues/77

最後,嘗試中文的 Speech to text 跟 text to speech ,再順便加一點點天氣回應,有興趣再到這邊看看,最大的關鍵點是用 AIY 的元件(arecord / aplay) 跟其他 gTTS / pydub 亂串時,發現有一些 async 狀態(要播放剛剛製成的音訊資料時,發現資料未完全寫入檔案,最後靠 time.sleep(3) 來解掉):

https://github.com/changyy/GoogleVoiceKitStudy/blob/master/detect-and-echo.py

2018年1月27日 星期六

[PHP] 使用 PHP built-in web server 及 PHP CodeIgniter framework

回想起來,我大概斷斷續續用了 CodeIgniter 七年了,最近才準備在本地開發 XD 開發上就在想 node.js 都有 webpack 等工具了,為啥 PHP 開發上還要先架設個 Web server 而感到納悶。

然而,其實 PHP 早已有 built-in web server 了,雖然只是 single-thread 但在開發上已經非常夠用,就來摸一下怎樣整再一起

$ cd project_web_document_root
$ php -S localhost:8000


接著就在 http://localhost:8000 就可以運行了!
然而,許多 web framework 都靠 routing 把 requests 統一集中到一支程式判斷,似乎已經是個非常基本的設計概念,那在 php built-in web server 也是可以的,他可以設定 routing 機制

$ cd project/web
$ cat ../tools/routing.php
$BASE_DIR = __DIR__ . '/../web' ;
if (file_exists($BASE_DIR . $_SERVER['REQUEST_URI']))
return false;
$_SERVER['SCRIPT_NAME'] = '/index.php';
include_once ($BASE_DIR . '/index.php');
$ php -S localhost:8000 ../tools/routing.php
PHP 7.0.27 Development Server started at Tue Jan 23 12:31:32 2018
Listening on http://localhost:8000
Document root is C:\Users\user\Desktop\ci-project\web
Press Ctrl-C to quit.


更多筆記:changyy/codeiginiter-and-php-built-in-web-server

2018年1月20日 星期六

AWS 筆記 - 使用 Ubuntu server 更換 Windows server 2012 帳號狀態

AWS Windows server

公司在三年前在 AWS Beijing 開了一台 Windows server 2012 運行一些套裝軟體,然而,負責的 IT 在整理系統帳號時,把 guest 關閉了。接著發生很多奇妙事件,最恐怖的是機器無法遠端登入了 Orz

一開始想朝更換 Administrator 密碼試試,結果一看過程步驟也不少:
使用 EC2Config 重置 Windows 管理员密码 -
https://docs.aws.amazon.com/zh_cn/AWSEC2/latest/WindowsGuide/ResettingAdminPassword_EC2Config.html
接著,反而朝惡搞的方式進去研究,例如:如何修改機碼啟用 guest 帳號


最後,使用了前一份(一年前版本)的全系統備份,開了一台機器,把他的 C:/Windows/System32/config/SAM 複製出來,覆蓋掉無法登入的機器上,如此一來就搞定了!

情況簡介:
A server: 壞掉的 Windows server 2012
B server: 從 A server 現狀建置 AMI 而開啟的機器
C server: 前一版次備份的 Windows server
D server: Ubuntu 14.04 server
A 計畫:
將 B server 的 C 槽卸下,並掛到 C server 上進行 B server 的 C槽 資料修改,改完後再卸下,重裝到 B server,把 B server 重新啟動,看看是否正常

然而,A 計劃失敗了,在 AWS console 可以看到系統無法正常通過檢驗,於是啟動了 B 計畫

B 計畫:
再次建置乾淨的 B server,將 B server 的 C 槽卸下,也把 C server - C 槽卸下,並一起掛到 D server 上,拿著 C server - C 槽關鍵資訊,直接覆蓋掉 B server - C 槽 資料,修改完後再卸下,重裝到 B server,再把 B server 重新啟動
果真 B 計劃成功了 :p 猜測 A方案 可能因為修改的是 Windows OS level 資訊,修改完可能也改變了 B server - C 槽狀態,以至於重新啟用時反而資訊對不上而無法開機。

AWS Windows server

其他筆記:

  1. Windows 需要 stop 機器,才能卸下指定硬碟,請開啟 “終止保護“ 避免不小心做錯事
  2. Windows 的系統碟(C:)根设备用 /dev/sda1 資訊,卸下後要重新裝上時,記得要用原本的 /dev/sda1 才行
  3. 在 EC2 - EBS 管理介面要卸下某硬碟時,建議把 Name 帶點原本的機器資訊,方便找尋
  4. 在 Ubuntu 要 mount NTFS 都還算簡單,sudo mount /dev/xsgf1 /mnt/Win2012CDisk 
  5. Windows 的帳號資訊擺在 C:\Windows\System32\config\SAM 檔案,可以單純做 cp /mnt/C_server_c_diskt/Windows/System32/config/SAM /mnt/B_server_c_diskt/Windows/System32/config/SAM 
  6. 驗證完畢後,就可以安心地把 A server 先暫停 -> 把系統槽卸下 -> 掛到 Ubuntu server -> mount 起來 -> 覆蓋掉 SAM -> 卸下 -> 掛回 A server -> A server 重新開機