Chia Yu Pai

Front-End, HTML5, Javascript, CSS3

Facebook oAuth redirect url on reverse proxy with nginx

| Comments

近來 Angular 的流行開始改用 Single Page Application 的架構作為網站基礎
前端為了速度直接採用 nginx 作為靜態檔案 server
後端透過 proxy_pass 的方式以 RegExp 判定特定 URL 達到同網域避開 Same Origin Policy 的問題
但遇到 oAuth 時候必須特別為他客製一條通道
否則在 URL Match Check 失敗會造成無限重導向 302

本來為了介接後端 Server 採用下方 nginx 設定

location ~ ^/api/(.*)$ {
  proxy_pass http://127.0.0.1:3000/$1;
}
# 自動轉換 http://yourdomain.com/api/foo/bar -> http://yourdomain.com:3000/foo/bar

此時為了 oAuth 的 Redirect URL 正確
如果使用 http://yourdomain.com/api/auth/facebook/callback 會被轉換成
http://yourdomain.com/auth/facebook/callback 而造成 URL not match 重新導入
最後就變成 302 無限重導向迴圈

最簡單的解決方式就是直接提供一條通道給 facebook callback 通過

location ~ ^/auth/facebook/callback$ {
  proxy_pass http://127.0.0.1:3000;
}

這樣就能正確的 Matching URL 又不改變本來的 API Route Scope
當然直接採用

location ~ ^/api/auth/facebook/callback$ {
  proxy_pass http://127.0.0.1:3000;
}

放在前面也可以,只是後方 Server 對於 Routing 的判斷就會有多一個 api

Anyway 其實這也不是什麼技術討論
只是被這個問題卡了一個晚上記錄一下
連 passport-facebook 的 source 都看了一遍才找到問題 lol

Angular Service Event

| Comments

AngularJS 的 Event 模組只能在 controller 中使用
當需要跨越不同的 controller scope 使用時
就無法直接使用內建的 $on, $emit 來實作 Event model
有兩種(或更多)解法來完成這樣的跨 controller 需求

Service variable and $scope.$watch

controller 的 $watch 可以監控 Service 的值
透過 injector service 便可做到不同 controller 之間的訊息溝通

angular.module('testApp')
  .controller('TestCtrl', function ($scope, Watcher) {
    $scope.$watch(function(){
      return Watcher.status;
    }, function (newStatus) {
        if (newStatus === 'done') {
        // ...do something

      }
    });
    // Remember:

    // if you want listen values in object, you have to add third parameter with true.

  )
  .controller('TestCtrl2', function ($scope, Watcher) {
    // ...some async code

    var callback = function () {
        Watcher.status = 'done';
    }
  )
  .service('Watcher', function () {
    this.status = null;
  
    return this;
  });
}

Service Scope

直接建立一個 Scope 來支援內建的 Event model
轉發他的 $on, $emit 來給其他 controller 使用

angular.module('testApp')
  .controller('TestCtrl', function ($scope, Watcher) {
    Watcher.$on('fetched', function () {
        // ...do something

    });
    
    // ...some async code

    var callback = function () {
        Watcher.$emit('fetched');
    }
  )
  .service('Watcher', function ($rootScope) {
    _scope = $rootScope.$new();
    
    this.$on = _scope.$on;
    this.$emit = _scope.$emit;
  
    return this;
  });

AWS EC2 ubuntu server vsftpd setting

| Comments

這兩天因為手邊空間不夠終於開了個 EC2 instance
不過設定個 vsftp 花了不少時間
其中不少眉角是跟其他的 VPS 不太一樣
紀錄一下也方便同樣遇到問題的人解答

首先因為預設的安裝包 ssh 登入需透過 RSA key 做登入的動作
但要啟用 ftp 功能 (SFTP 當然就不用他是 ssh tunnel) 需要有密碼才能進行登入的動作
也不建議使用預設有 root 權限的帳號登入
所以請新增個帳號並給他密碼吧

sudo useradd -m ftpuser; sudo passwd ftpuser

然後請進行安裝 vsftpd 的動作,以 ubuntu 為例

sudo apt-get install vsftpd

接著請到安裝目錄尋找 vsftpd.conf 調整下列項目

anonymous_enable=NO  # 禁止匿名登入

local_enable=YES             # 啟用本機帳號登入

write_enable=YES         # 允許寫入


# 這裡要啟用 Passive 模式

# 設定一個區間的 port 讓 ftp 可以開多個 connection 同時傳檔

pasv_enable=YES
pasv_max_port=35000
pasv_min_port=34000
port_enable=YES

如果要設定 chroot 請參考網路相關文章這裡就先不談
接著我們需要對 iptables 進行設定開啟這些 port 的權限
ftp client 才可以傳送檔案

iptables -I INPUT -p tcp --destination-port 34000:35000 -j ACCEPT

還有個問題如果你有設定 chroot 新版的 vsftpd 規定
chroot 時該帳號的家目錄(ftp 根目錄)必須為不可讀
所以允許登入的 user home 需要做權限調整

sudo chmod -w /home/ftpuser

別忘了需要重開 vsftpd

Ubuntu:

sudo service vsftpd restart

最後因為 AWS 對於 port 需要另外在 console 進行設定
請到 Security Groups 開啟一組設定
並允許 Inbound tcp port
20-21
34000-35000

接著就可以透過 ftp 來連接你的 EC2 囉!

Yo Angular with CoffeeScript / Stylus / Jade

| Comments

2014-04-11 更新

我 Fork 出來一個可以自動產生的版本了
generator-angular-jade-stylus (GitGub)


yeoman 是十分方便的前端網頁產生模式,透過 angular generator 可以快速的產生良好架構的前端網頁,但 yo angular 預設支援 SASS / pure HTML 跟我的開發習慣不太吻合,研究了一下在 Grunt 加入自動編譯 Stylus / Jade 的方法。

此外,如果要在 generator-angular 啟用 CoffeeScript 請記得在 yo angular [appName] 就要加入 --coffee 參數,才會產生 Coffeescript 版的範例網頁唷。

generator-angular (GitHub)

首先我們需要載入 Grunt Contibutor for Stylus / Jade

npm install grunt-contrib-jade -D
npm install grunt-contrib-stylus -D

接著修改 Gruntfile.js 的配置

於最上方引用外部的 Task 並註冊給 compass

module.exports = function (grunt) {
  grunt.loadNpmTasks('grunt-contrib-stylus');
  grunt.loadNpmTasks('grunt-contrib-jade');
  grunt.registerTask('compass', ['stylus']);
  
  grunt.initConfig({
  //.....

}  

之後就要註冊我們自己的 Task Config

grunt.initConfig({
  jade: {
    dist: {
      options: {
        pretty: true
      },
      files: [{
        expand: true,
        cwd: '<%= yeoman.app %>',
        dest: '.tmp',
        src: ['*.jade', 'views/{,*/}*.jade'],
        ext: '.html'
      }]
    }
  },
  
  /*
  這裡我使用了 compass 協助我合併多個 stylus 的檔案,節省在引用時的 request 數量
  files 的 key 就是合併後的檔案位置 .tmp 是預設的 generator-angular test assets folder
  value 則是 source sheets 的陣列並可使用通用字符
  */
  
  stylus: {
    compile: {
      options: {
        compress: true,
        paths: ['node_modules/grunt-contrib-stylus/node_modules']
      },
      files: {
        '.tmp/styles/main.css': ['<%= yeoman.app %>/styles/*.styl']
      }
    }
  },
  
  /*
  為了可以即時透過 livereload 檢視修改的檔案,也在 watch 中加入監聽條件
  */
  
  watch: {
    jade: {
      files: ['<% yeoman.app %>/*.jade'],
      tasks: ['jade']
    },
    stylus: {
      files: ['app/styles/**/*.styl'],
      tasks: ['stylus']
    },
    // ...

  },
  
  /*
  接著因為我把 HTML 改成 Jade 編譯,所以原本 app 資料夾中僅有 source 檔案,build 過的檔案已經改到 .tmp 中了,要使 bower-install / usemin 可以正常運作在 build 時合併檔案,需要修改一些預設路徑。
  */
  
  'bower-install': {
    app: {
      html: '.tmp/index.html',
      ignorePath: '<%= yeoman.app %>/'
    }
  },
  
  useminPrepare: {
    html: '.tmp/index.html',
    options: {
      dest: '<%= yeoman.dist %>'
    }
  },
  
  // ...

最後只要在 grunt serve / build 的 task 裡面加入 jade 的 call 就可以囉

grunt.registerTask('serve', function (target) {
  if (target === 'dist') {
    return grunt.task.run(['build', 'connect:dist:keepalive']);
  }
  
  grunt.task.run([
    'clean:server',
    'jade',
  // ...

  ]);
});

// jade 的位置很重要,你必須在 usemin 之前先將 html 編譯完成,才可以正常的進行最小化以及 bower-install 的動作


grunt.registerTask('build', [
  'clean:dist',
  'jade',
  // ...

]);

以上就是在 generator-angular 加入 Jade / Stylus 的方法,這樣的修改方法會遇到個問題,就原本的用法 yo angular:controller 等新增動作時,會自動加入 index.html 的引用句會無法使用,因此你將必須手動將

至於修改這個 Bug 的方法,必須直接修改 generator 的 source 本身,這樣每當 generator 更新版本都需要再次修改,等於要維護一個新的 fork 在時間有限的情況下就先將就著用囉,詳細改法有興趣可以來信討論 :)

--
附上一個修改後的 index.jade 方便大家直接套用

doctype html
<!--[if lt IE 7]>
<html class="no-js lt-ie9 lt-ie8 lt-ie7">
<![endif]-->
<!--[if IE 7]>
<html class="no-js lt-ie9 lt-ie8">
<![endif]-->
<!--[if IE 8]>
<html class="no-js lt-ie9">
<![endif]-->
<!--[if gt IE 8]><!-->
<html class="no-js">
<!--<![endif]-->
head
  meta(charset="utf-8")
  meta(http-equiv="X-UA-Compatible", content="IE=edge")
  title
  meta(name="description", content="")
  meta(name="viewport", content="width=device-width")
  // Place favicon.ico and apple-touch-icon.png in the root directory
  // build:css styles/vendor.css
  // bower:css
  link(rel="stylesheet", href="bower_components/bootstrap/dist/css/bootstrap.css")
  // endbower
  // endbuild
  // build:css({.tmp,app}) styles/main.css
  link(rel="stylesheet", href="styles/main.css")
  // endbuild
body(ng-app="cominfinitibeatstyletripplacewebApp")
  <!--[if lt IE 7]>
  p.browsehappy
    | You are using an 
    strong outdated
    |  browser. Please 
    a(href="http://browsehappy.com/") upgrade your browser
    |  to improve your experience.
  <![endif]-->

  // Add your site or application content here 
  div.container(ng-view="")

  // Google Analytics: change UA-XXXXX-X to be your site's ID 
  script.
    (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
    (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
    m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
    })(window,document,'script','//www.google-analytics.com/analytics.js','ga');

    ga('create', 'UA-XXXXX-X');
    ga('send', 'pageview');

  <!--[if lt IE 9]>
  script(src="bower_components/es5-shim/es5-shim.js")
  script(src="bower_components/json3/lib/json3.min.js")
  <![endif]-->

  // build:js scripts/vendor.js 
  // bower:js 
  script(src="bower_components/jquery/jquery.js")
  script(src="bower_components/angular/angular.js")
  script(src="bower_components/bootstrap/dist/js/bootstrap.js")
  script(src="bower_components/angular-resource/angular-resource.js")
  script(src="bower_components/angular-cookies/angular-cookies.js")
  script(src="bower_components/angular-sanitize/angular-sanitize.js")
  script(src="bower_components/angular-route/angular-route.js")
  // endbower 
  // endbuild 

  // build:js({.tmp,app}) scripts/scripts.js 
  script(src="scripts/app.js")
  script(src="scripts/controllers/main.js")
  // endbuild 

最下方的 // endbuild 之上就是我提到要自行加入 script tag 的地方哦。

WebRTC Peer to Peer With Socket.io

| Comments

知識

WebRTC 是最新的瀏覽器應用之一,提供 Web Application Client 彼此互連的功能實作基礎
在標準之中,主要有三大 API 項目

  1. MediaStream
  2. RTCPeerConnection
  3. RTCDataChannel

其中又以 RTCPeerConnection 為整個協定的基礎
WebRTC 就像是稍早的 WebSocket 一般,不同於 stateless HTTP 協定
而是較像是傳統語言的 Socket,使用較精簡的協定內容,讓反應速度可以提升到很高的水準
另外本文所有語法使用 Coffeescript 撰寫,相關訊息請洽官方網站

API

MediaStream

這個 API 其實就是 getUserMedia 的正式名稱,提供開發著一個統一的介面存取使用者的多媒體介面
舉凡視訊鏡頭、麥克風及螢幕節圖等,支援度在各大瀏覽器也算中上
需要多媒體應用的可以多加研究,在此因為非關 WebRTC 核心部分就不多提了
詳細用法可參考 MDN 的說明
Navigator.getUserMedia - MDN

RTCPeerConnection

如同其名,這個物件算是整個協定的核心項目
因為過去 WebRTC 的標準還沒訂定,網路上很多各種不同的資訊十分凌亂
基本上,當兩台 Client 需要做直接連線時,理想上我們可以直接透過 IP 互連的方式
取得最快的 Route,但是因為網際網路實際的發展並非如此
在我們的電腦前眾多的 NAT / IP Router 等打亂了簡單的 IP 直連可能
於是我們必須借助 STUN Server 替我們記錄真實的轉送流程
才能正常送到目標的對象手上,目前 Google 有提供一組免費的 STUN Server 如下
stun:stun.l.google.com:19302

除了 IP Route 的問題之外
我們要如何找到對方存在呢?使用 DHT 等無中央式的網路似乎可行
但是這樣會大幅拉長配對時間,就失去的 WebRTC 相較於傳統 HTTP 連線的最大優勢:速度
因此我們還是透過中央伺服器的方式協助我們的使用者做交換訊息的工作
在此就以 Socket.io 作為範例,解釋整個流程

首先,WebRTC 的 Client 端會分為 Offer 與 Answer 兩種角色
如同字面上意思 Offer 端為連線的發起者,Answer 則為被動方的角色
在啟用一個新的 WebRTC Connection 時,我們必須建立一個 Offer,並取得它的 Description

RTCPeerConnection = RTCPeerConnection or webkitRTCPeerConnection or mozRTCPeerConnection
# 提供 STUN Server
iceServers =
  iceServers: [{
    url: 'stun:stun.l.google.com:19302'
  }]

# 啟用 RTPDataChannel
optionalRtpDataChannels =
  optional: [{
    RtpDataChannels: true
  }]

# 建立一個新的 Connection
peer = new RTCPeerConnection iceServers, optionalRtpDataChannels

# 建立 Offer 方法
createOffer = ->
  # 建立 DataChannel 接管資料的傳輸
  dataChannel = peer.createDataChannel 'RTCDataChannel',
    reliable: false
  
  # 這是一些事件的監聽與錯誤檢測
  channel.onmessage = (event)->
    console.debug "offer receive a message #{event.data}"

  channel.onopen = ->
    console.debug 'channel open'

  channel.onclose = (e)->
    console.error e

  channel.onerror = (e)->
    console.error e
   
  # 透過 Peer 建立 Offer
  peer.createOffer (sessionDescription)->
    # 設定本地端 Description
    peer.setLocalDescription sessionDescription
    
    # 傳送回 Server 本地端的 Description
    socket.emit 'sourceOffer', 
      description: sessionDescription  
  , null, {}

createOffer 提供三個參數,首先當然是產生了 Offer Description 後的 callback 事件
第二個是如果產生失敗時的 Error callback,最後則是對於多媒體的限制與選項 MediaConstraints
別忘了,伺服器端需要使用 Socket.io 轉送這個 Description 給目標連線對象

接著,接收方則需要建立一個 Answer 去準備相關的回應

socket.on 'sourceOffer', (data)->
  # 接收時記得把原始物件轉換為 RTCSessionDescription 的實例
  answerSDP = new RTCSessionDescription data.description
  createAnswer answerSDP

# 建立 Answer 方法
createAnswer = (offerSDP)->
  # 一樣建立 DataChannel 接管資料的傳輸
  dataChannel = peer.createDataChannel 'RTCDataChannel',
    reliable: false

  ###
  略過事件監聽與除錯部分,請參考 Offer 的設定
  ###

  # 設定好遠端的 Description
  peer.setRemoteDescription offerSDP
  
  # 建立 Answer
  peer.createAnswer (sessionDescription)->
    # 一樣先儲存 Local Description
    peer.setLocalDescription sessionDescription

    # 把本地端的 Description 丟回給 Offer
    socket.emit 'sourceAnswer', 
      description: sessionDescription.description
  , null, {}

最後我們在回到 Offer 發起端

socket.on 'sourceAnswer', (data)->
  # 一樣轉成 RTCSessionDescription
  answerSDP = new RTCSessionDescription data.description
  # 設定為遠端的 Description
  peer.setRemoteDescription answerSDP

到這裡,我們已經完成基本的辨識
offer.localDescription = answer.remoteDescription
answer.localDescription = offer.remoteDescription
兩方各持有對方的 Description 描述

接著我們要使用 STUN Server 給我們的 candidate 建立連線
所以需要在 peer 加入監聽事件,將拿到的 candidate 轉送給另一方

# ICE
peer.onicecandidate = (event)->
  return if !peer or !event or !event.candidate
  socket.emit 'candidate', event.candidate

所以當然也有接收方

socket.on 'candidate', (data)->
  # 將 candidate 給 peer 讓他找到我們的路徑
  peer.addIceCandidate new RTCIceCandidate
    sdpMLineIndex: data.candidate.sdpMLineIndex
    candidate: data.candidate.candidate

至此連線就建立完成
複習一下,
Offer -> Answer -> Offer -> Offer Candidate <-> Answer Candidate

RTCDataChannel

最後其實關於它的基本東西都說完了
在連線建立起來後它負責的就是傳輸的部份了
透過 send 方法可以自由的在雙方傳送資訊
對方就會在 channel.onmessage 收到囉!


更多的問題歡迎留言討論
也可參考 W3C 官方的標準說明書

angular 1.2 ngRoute

| Comments

angular 在 1.16 版後將原本的 $route, $routeProvider, $routeParams 獨立出來
從 1.0.x 升級的使用者會無法直接使用原本的 Route Service
必須在對 app 引入 ngRoute module 方可使用
此外 ngRoute 的代碼也是獨立出來,不再包在 angular.js 之中

新的使用方法如下:

script(src='http://ajax.googleapis.com/ajax/libs/angularjs/1.2.0-rc.3/angular.min.js')
script(src='http://code.angularjs.org/1.2.0-rc.3/angular-route.min.js') 

<script>
angular.module('yourAppName', ['ngRoute'], function ($routeProvider){
    $routeProvider
    .when('/', {
        controller: yourController
      templateUrl: templateFileRoute
    });
});
</script>

angular-service,-provider,-factory

| Comments

三種Angular.Module建構函式的差異

Service

建構式中傳入的 this 可以任意給予 Property/Method
當Controller引用的時候會直接將這個this scope回傳使用,算是最基本的包裝

Factory

Controller引用的時候會將建構式的回傳值物件(Return Object)包裝
在建構時可以不用擔心this scope的汙染問題

Provider

Getter的概念
不同於Service回傳整個建構式的this scope只會回傳this.$get的內容
此外this.$get需為一個函式,於Controller引用的時候會即時call function
可以透過私有函數的方法輕易弄成迭代器(iterator)

網路上有人寫好的Gist Example
https://gist.github.com/Mithrandir0x/3639232

Javascript Getter / Setter

| Comments

Modern browsers' (IE9 up, IE8 only on DOM Object) Javascript Engine supported Getter / Setter for Object

IE6-8 could listen onpropertychange event

我們有兩個常見的方法去實作,例子如下:

Sample A:
透過 __defineSetter____defineGetter__ 預設方法榜定

Dog = function (name) {
    this.name = name || 'Doggy';
  
  // Define setter with __defineSetter__ method.

  this.__defineSetter__("name", function(newVal){
    this.bark('My new name is ' + newVal);
  });
}

Dog.prototype.bark = function (msg){
    msg = msg || "I'm " + this.name;
    alert('Bark! ' + msg);
}

Carl = new Dog();
Carl.name = Carl // Alert: Bark! My new name is Carl

Carl.name // Carl

Sample B:
直接使用 setget 關鍵字定義

Dog = function (name) {
    this._name = name || 'Doggy'; // Default name without alert.

}

Dog.prototype = {
    bark: function (msg) {
    msg = msg || "I'm " + this.name;
        alert('Bark! ' + msg);
  },
  // Define Setter

  set name(newVal) {
    this._name = newVal;
        this.bark('My new name is ' + newVal);
  },
  // Define Getter

  get name() {
    return this._name;
  }
}

Carl = new Dog();
Carl.name = Carl // Alert: Bark! My new name is Carl

Carl.name // Carl

透過 Setter / Getter 的設定可以輕鬆的監聽物件方法的改變,目前最常用到的非屬 two-way binding 的專案。可以直接用十分抽象的方法讓使用者改變 Javascript 中物件的屬性值來換取 DOM 的直接內容變化,達到驚人的易用水準。

Frameworks: