RaspberryPi
Basic認証
MJPG-streamer
Swift

mjpg-streamerのページにBASIC認証を掛ける+iOSアプリ上からでも認証を通して画像を見られるようにする話

More than 3 years have passed since last update.

前々回:
RaspberryPi - Raspberry piとApacheとWebカメラで外部から見られる監視カメラを作った話。
http://qiita.com/CST_negi/items/a329cc98fb1aa33f33d3

前回:
Xcode6 - (初心者向けiPhoneアプリ開発)raspberry piで取得した監視カメラの画像をiPhone上で見る話。
http://qiita.com/CST_negi/items/882f1e1c3bede8f8ccf3

ここまでいろいろやってきて、さすがにパスワードかかってないのはまずいでしょ…という感じになってきたのでパスワードをかけました。


やりたかったこと:
・mjpg-streamerの配信にBASIC認証をかけて簡単には外部から見られないようにする。
・iOSアプリのほうでも認証できるようにして画像を取得できるようにする。

今回の記事では2つのことをやります。
・mjpgstreamerにBASIC認証を追加させ、それを起動するためにシェルスクリプトを一部変更する
・iOSアプリのほうでBASIC認証を成功させて画像を取得するためにSwiftコードの変更

では書いていきます。そんなに大変じゃないので皆さんも面倒くさがらずやると良いです。


・mjpgstreamerにBASIC認証を追加させ、それを起動するためにシェルスクリプトを一部変更する

前々回の記事では起動にあたってシェルスクリプトを書くと良いかもですねという話をしていたのですが、実際には以下のようなものを書いていました。

mjpg-streamer.sh
#!/bin/sh                                                                      

ACTION=$1
DEVICE=$2

start_action() {
./mjpg_streamer -i "./input_uvc.so -f 2 -r 320x240 -d /dev/video0 -y" -o "./o\
utput_http.so -w ./www -p 8081"                                                
}

stop_action() {
  kill -9 `pidof mjpg_streamer`
}

case $ACTION in
  start)
       start_action
       ;;
   stop)
       stop_action
       ;;
   *)
       ;;
esac

これを使って
sh mjpg-streamer.sh start
とやるだけで、実行できるのと
sh mjpg-streamer.sh stop
で停止できるのが便利した。
(参考:http://albertlabo.wiki.fc2.com/wiki/mjpg-streamer)

何をしているかというと、単純にポート番号8081でストリーミングサーバーを起動しているだけですね。

ここから変更点のお話をします。
先ほどのシェルスクリプトではストリーミングサーバー起動の際にBASIC認証が無効になっているので、有効化するためにここに認証をかけるオプションを追加していきます。

なので以下のようにstart_action()を変更してください。

mjpg-streamer.sh
start_action() {
sudo ./mjpg_streamer -i "./input_uvc.so -d /dev/video0 -r 320x240 -f 5 -q 50 -\
y -n" -o "./output_http.so -w ./www -p 8081 -c [任意のID]:[任意のパスワード]"                                           
}

これで認証をかけることができました。簡単!
(参考:http://qiita.com/yoh-nak/items/723c2a6a1d83c63198d7)
(参考:http://dkpyn.com/blog/%E3%83%A1%E3%83%A2/linux/usb%E3%82%A6%E3%82%A7%E3%83%96%E3%82%AB%E3%83%A1%E3%83%A9%E3%81%A8ubuntu%E3%81%A7%E7%B0%A1%E5%8D%98%E3%82%B9%E3%83%88%E3%83%AA%E3%83%BC%E3%83%9F%E3%83%B3%E3%82%B0%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC)

このシェルスクリプトを起動して改めてブラウザなどでストリーミングサーバーにアクセスします。
(http://設定したDDNSのホスト名:8081/javascript_simple.html とかそんな感じ)

すると
basicninsyou.png
のようないつもの認証画面がでてきます。
ここに
ユーザ名:[任意のID]
パスワード:[任意のパスワード]
を入力してログインするといつものようにカメラからの画像を見ることができます。

ここまでで半分終わりです。
次からiOSアプリ側でどうやって認証を通すかの話です。


BASIC認証の話
BASIC認証がかかっているサイトにアクセスする際にURLに直接IDとパスワードを書いてアクセスすると認証画面が出ずにアクセスできるのを皆さんご存知でしょうか?

つまり、ベーシック認証がかかったサイトで
ユーザー名:user
パスワード:password
とすると、
http://user:password@www.hoge.com/
で、直接アクセスすることが出来るということです。

(参考:http://qiita.com/ngkazu/items/6da021edf177f40e1f26)

これを使えばiOSアプリ上からでもいけるのでは?と思ってやってみた話が次になります。


・iOSアプリのほうでBASIC認証を成功させて画像を取得するためにSwiftコードの変更
(失敗Take)

※以降は、話をわかりやすくするために
サイト名:example.com:8081
ユーザ名:userid
パスワード:pass
とします。

前回の記事でSwiftを書いたときに、UIWebViewに画像を渡すコードは

ViewController.swift
func loadCamView(){
        var url: String = "http://example.com:8081/?action=snapshot"
        let requestURL = NSURL(string: url)
        let req = NSURLRequest(URL: requestURL!)

        MonitorWebView.loadRequest(req)
    }

となっていました。で、先ほどのBASIC認証の話を元に

ViewController.swift
func loadCamView(){
        var url: String = "userid:pass@negipoyoc.dip.jp:8081/?action=snapshot"
        let requestURL = NSURL(string: url)
        let req = NSURLRequest(URL: requestURL!)
        camWebView.loadRequest(req)
    }

としてみました。これでビルドすると!画像が!出てくる!

となったらよかったですね。
この方法だとWebViewに画像は出てきませんでした。
現実はそんなに甘くないということですね。

ただ、そこまで辛くもないです。


・iOSアプリのほうでBASIC認証を成功させて画像を取得するためにSwiftコードの変更
(成功Take)

じゃあどうすればいいの?ということなんですが、Basic認証のためのヘッダー付き Requestオブジェクトを作成する方法はちゃんとありましたので、そちらを使います。

カメラ画像のロード関数のコードを以下のように変更します。

ViewController.swift
func loadCamView(){
    var url_with_basic_auth = "http://example:8081/?action=snapshot"
    var url = NSURL(string: url_with_basic_auth)
    var req = NSMutableURLRequest(URL: url!)

    var username = "userid"
    var password = "pass"
    var authStr = "\(username):\(password)"
    var data = authStr.dataUsingEncoding(NSUTF8StringEncoding)

    var authData = data!.base64EncodedStringWithOptions(NSDataBase64EncodingOptions.allZeros)
    var authValue = "Basic \(authData)"

    //作成したAuthorizationヘッダーの付与
    req.setValue(authValue, forHTTPHeaderField: "Authorization")

    camWebView.loadRequest(req)
    }

(参考:http://qiita.com/nyamage/items/9f24488419c42888c7c7)

やっていることは
Basic認証のためのヘッダー付き Requestオブジェクトを作ってそのリクエスト結果を前回同様、WebViewに表示させているだけです。

これにてめでたく、認証を通して画像を表示させることができたと思います。お疲れさまでした。


おまけ
UserIdとPasswordをハードコーディングするのはあまりにも気持ち悪いので僕のアプリでは、以下のようなTabViewを作ってみました。

iPhoneDisplay.png

以下はそれをどう実現したかのコードの説明なので見たい方だけどうぞ!
(Swift書き始めてまだ1週間とかなので、ここはこうしたほうがいいんじゃない?みたいな話があったらおしえてください。)

SettingViewController.swiftは名前の通り設定ビューのコントローラです。
テキストフィールドへの入力を保存します。
また、アプリが起動されてこのビューが表示されたときにユーザ名とパスワードをロードします。
そのため一度変更し保存したら、特に再起動しても再設定の必要はありません。

SettingViewController.swift
class SettingViewController: UIViewController, UITextFieldDelegate {

    @IBOutlet weak var MonitorUserNameTextField: UITextField!
    @IBOutlet weak var MonitorPasswordTextField: UITextField!
    let settingManager = SettingManager()



    override func viewDidLoad() {
        //Xcodeのバグのため必要
        self.tabBarItem.selectedImage = UIImage(named: "Setting_PDF")

        //監視カメラのユーザ情報などをロードする
        var userName = settingManager.SettingLoad("MonitorUserName")
        var password = settingManager.SettingLoad("MonitorPassword")

        //@Unknownはロードできなかったときに返されるString値
        MonitorUserNameTextField.text =
        userName == "@Unknown" ? "Please Set UserName" : userName

        MonitorPasswordTextField.text =
        password == "@Unknown" ? "" : password

        //Returnキーを押した際にキーボードを閉じるための関数
        //(参考:https://akira-watson.com/iphone/textfield.html)
        MonitorUserNameTextField.delegate = self
        MonitorPasswordTextField.delegate = self

    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    @IBAction func MonitorUserNameTextField_EditingDidEnd(sender: UITextField) {
        var userName = MonitorUserNameTextField.text
        settingManager.SettingSave(userName, keyName : "MonitorUserName")
    }


    @IBAction func MonitorPasswordTextField_EditingDidEnd(sender: UITextField) {
        var password = MonitorPasswordTextField.text
        settingManager.SettingSave(password, keyName: "MonitorPassword")

    }

    //Returnキーを押した際にキーボードを閉じるための関数
    func textFieldShouldReturn(textField: UITextField) -> Bool{
        textField.resignFirstResponder()
        return true
    }
}

SettingManager.swiftはアプリが終了してもこの設定を残しておくようにするための関数を詰め込んでいます。
(参考:http://qiita.com/m-tsuchiya/items/5e919813be19fb4b53bf)

SettingManager.swift
import UIKit

class SettingManager {
    let config = NSUserDefaults.standardUserDefaults()

    func SettingSave(value : String , keyName : String){
        // 設定値の保存
        config.setObject(value,forKey:keyName)
        config.synchronize() // シンクロを入れないとうまく動作しないときがあります
    }


    func SettingLoad(keyName : String) -> String{
        // 設定値の取得
        let result : AnyObject! = config.objectForKey(keyName)

        // 存在しないキーはnilが返る            
        if result == nil {
            //もしkeyに対応するものがなかったら"@Unknown"を返す。
            return "@Unknown"
        }
        // AnyObjectからString型にダウンキャストするときは as NSString
        var val:String = result as! NSString as String 
        return val;

    }
}

これを踏まえて、先ほどのWebViewがつかわれているビューではViewController.swiftでは

var username = "userid"
var password = "pass"

から

var username = settingManager.SettingLoad("MonitorUserName")
var password = settingManager.SettingLoad("MonitorPassword")

に変更しています。

以上です。ありがとうございました。