AllIsHackedOff

Just a memo, just a progress

Goで静的ファイルのバイナリ埋め込み

go-bindata が死んでからあんまりやっていなかったけどAWS Lambda上でテンプレートファイルを読んで処理するプログラムを書くことになり、 一時ファイルの置き場やらデプロイやらで面倒なことをしたくなかったのでシングルバイナリにまとめることに

github.com

悪くない。テンプレートはParseするときに普通のstringを受け取れるので、テンプレートファイルを扱うときも適当にかけば動く

main.go
templates/
    admin/
          index.html
import (
    "fmt"
    "html/template"
    "os"

    "github.com/gobuffalo/packr"
)

func main() {
    box := packr.NewBox("./templates")

    s := box.String("admin/index.html")
    t := template.New("admin/index")
    t, err := t.Parse(s)
    if err != nil {
        panic(err)
    }
    err = t.Execute(os.Stdout, map[string]string{
        "Hoge": "Variable hoge",
    })
    if err != nil {
        panic(err)
    }
    fmt.Println("______________")
    fmt.Println(s)
    fmt.Println("--------------")
<html>
    <head>
        <title>admin</title>
    </head>
    <body>
        Variable {{ .Hoge }}
        amdin dayo ---- aaaaaaaaa
    </body>
</html>

GoでSemantic Versionを適当にparseするパッケージ

久々の更新 今回Semantic Versionをparseするパッケージ探していて思ったのですが、awesome-goもアレげだし、普通のgopher勢は 欲しいパッケージをどこで探しているのだろうか? Gopher勢の場合、自前で作っちゃう勢が多そうなイメージがありますがRubyでいうToolbox的なのがあると便利なんだけどなと思ったり。

handling Semantic Versioning in Golang

Goでバージョンを適当に扱う必要がありパッケージを探したところhashcorp製の GitHub - hashicorp/go-version: A Go (golang) library for parsing and verifying versions and version constraints. この子を見つけた。

NewVersion がerrを返すの若干使いづらさを感じるが、別にmap系の関数で移したいわけではないのでよく考えると特に問題はない。 サンプルコード通りだがかきの感じで動く、Versionの昇順がデフォルト sort パッケージに修正が入ってからは Reverse が~とか考えなくて良くなったので非常に良い...

package main

import (
    "fmt"
    "sort"

    "github.com/hashicorp/go-version"
)

func main() {
    versionsRaw := []string{"1.1", "0.7.1", "1.4-beta", "1.4", "2"}
    versions := make([]*version.Version, len(versionsRaw))
    for i, raw := range versionsRaw {
        v, _ := version.NewVersion(raw)
        versions[i] = v
    }

        // この辺、すごい昔にReverseするのがクソめんどくさかった覚えがある 
    sort.Slice(versions, func(i, j int) bool {
        isLess := versions[i].LessThan(versions[j])
        return !isLess
    })

    for _, v := range versions {
        fmt.Println(v.String())
    }
}

GoでDynamoDBのUpdateItemを呼ぶ場合の例

DynamoDBの更新は2種類存在する

  • PutItem

    Creates a new item, or replaces an old item with a new item. If an item that has the same primary key as the new item already exists in the specified table, the new item completely replaces the existing item. You can perform a conditional put operation (add a new item if one with the specified primary key doesn't exist), or replace an existing item if it has certain attribute values. You can return the item's attribute values in the same operation, using the ReturnValues parameter.
    Putはリソースの完全な置き換えを意味する。HTTPのPutと同じ意味合いだ。

  • UpdateItem

    Edits an existing item's attributes, or adds a new item to the table if it does not already exist. You can put, delete, or add attribute values. You can also perform a conditional update on an existing item (insert a new attribute name-value pair if it doesn't exist, or replace an existing name-value pair if it has certain expected attribute values).
    You can also return the item's attribute values in the same UpdateItem operation using the ReturnValues parameter.
    一方でupdateは存在しない要素の追加、条件付きアップデートなどを意味する。 サービス内のデータを「更新」したいときは基本的には差分更新を示すので、こちらの意味合いになる。

参考

DynamoDBのputItemとupdateItemの違い | ハックノート

guregu/dynamodbを使う

気楽にDynamoDBを使おう - Qiitaにあるようにaws-sdk-goを利用するとよくわからない記法でUpdate文を作ることになり「こんなことするために生まれてきたわけではないのに」的な気分に落とし込まれる。guregu氏のライブラリを用いることで直感的にDynamoDBを操作できる。

Updateを実行するためには下記のメソッドでUpdate構造体を生成するのだが、ノリでコーディングしていると引数として何を与えれればいいかワカラナイ…

func (table Table) Update(hashKey string, value interface{}) *Update {

だめな例として

## { "id" : 1, "msg": "Maimi love"} を {"id": 1, "msg": "Kamiko Love"} に書き換えたいとする
## idをHash Key (Primary Key)とする
m.msg = "Kamiko"
u := tbl.Update(strconv.Itoa(m.id, msg) 

### ValidationException: One of the required keys was not given a value

そもそもPrimary KeyがStringじゃなきゃ駄目なのかよ???みたいな疑問が湧いたり等し、ぐぐってexampleを探すも見つからずに時間を浪費する的なことになるので一旦updateItemの公式の記法に当たります。 UpdateItem - Amazon DynamoDB

### Sample Requestより抜粋
{
    "TableName": "Thread",
    "Key": {
        "ForumName": {
            "S": "Amazon DynamoDB"
        },
        "Subject": {
            "S": "Maximum number of items?"
        }
    },
    "UpdateExpression": "set LastPostedBy = :val1",
    "ConditionExpression": "LastPostedBy = :val2",
    "ExpressionAttributeValues": {
        ":val1": {"S": "alice@example.com"},
        ":val2": {"S": "fred@example.com"}
    },
    "ReturnValues": "ALL_NEW"
}      

ということでKeyに対してUpdateしたい値を与えていけば更新することができそうな予感がします。 guregu/dynamoDBにおいてはupdate構造遺体の生成時に1つのキーに対して何かしらの interface{}を割り当てているのでこの記法は使えなそう?

ということで公式の別の例を見ます。

{
    "TableName": "Thread",
    "Key": {
        "ForumName": {
            "S": "Amazon DynamoDB"
        },
        "Subject": {
            "S": "A question about updates"
        }
    },
    "UpdateExpression": "set Replies = Replies + :num",
    "ExpressionAttributeValues": { 
        ":num": {"N": "1"}
    },
    "ReturnValues" : "NONE"
}

UpateExpressionとExpressionAttributeValuesを与えてやることでConditional Updateができるようです。 Guregu/dynamoDBではupdate.go - guregu/dynamo - SourcegraphこのあたりのメソッドでUpdateExpressionを生成していることを鑑みて、よしなに生成されるように書けばよさそうです。

u := tbl.Update("id", m.id)
u.Set("msg", m.msg)
err := u.Run() 

こんな感じに書くことでうまくUpdateできるようです 尚、手元での動作確認には

が便利です。

スマートスピーカーに青空文庫を朗読させるためにATOMのプラグインを作っている話

こんばんは。この記事はGunosy Advent Calendar 2017 - Qiitaの12/22の記事です。 前日の記事はGoogle Home と Android ThingsでLチカする - CrossBridge Labでした。 本末転倒?感のあるタイトルをつけてしまいましたが、 今回はGoogle HomeやらAmazon Echoに青空文庫を朗読させるために余暇で行っている下準備について記載します。

僕の頭のなかではAmazon Echoに青空文庫を朗読しながら眠りにつく毎日を送っているはずだったのですが。 Amazon Echo ... 申込んだのになかなか売ってもらえない....つらい。 実機がない状態でLambdaやPollyを触りつつ、所持しているGoogle Homeで粛々と準備をすすめている次第です。

目次

  • SSML(Speech Synthesis Markup Language)について
  • Amazon Polly
  • SSMLを気軽に作りたい -> Atomプラグイン作成

SSML(Speech Synthesis Markup Language)について

SSMLはSpeech Synthesis Markup Languageの略称です。その名の通り、音声合成マークアップするための言語で、仕様はSpeech Synthesis Markup Language (SSML) Version 1.1に記載されています。 SSMLを記述することにより会話中の

  • 休止(break / 秒数)
  • 強調(emphasis)
  • 声量/ピッチ/強弱/速さ (prosody)

をコントロールすることができます。

Alexaで利用できるタグに関しては下記の公式ドキュメントに非常に丁寧に記載されており、日本語の発話例も提示されています。 Speech Synthesis Markup Language (SSML) Reference | Custom Skills Speechcon Reference (Interjections): Japanese | Custom Skills

12/20に公開された下記の記事に具体例と共にGoogle Assistantの設定まで詳しく書かれており、非常に勉強になりました。 qiita.com

Amazon Polly

Amazon PollyはText-to-Speech用の音声を合成してくれるクラウドサービスです。 https://aws.amazon.com/jp/blogs/news/polly-text-to-speech-in-47-voices-and-24-languages/ AWS Consoleから気軽に試すことができ、無料利用枠も存在しています。

試しに青空文庫から宮沢賢治 狼森と笊森、盗森を取得してGUI上にコピペしてみると f:id:masashisalvador:20171222191624p:plain こんな感じになります。 soundcloud.com 青空文庫の文章を色々投げ込んでみて感じた所感としては、意外と発音は正しい、人名やコンテキストによって読みが変わる文字、ルビがないと人間にも読めないものはやはりうまく発音されません。 (上記の例だと、巨い(おおきい)など) そして何より、素の発音だと抑揚がないため、ぶっちゃけるとあまり面白くありません。

いい朗読をさせるためにはSSMLをうまく書いてやらないといけない。 ということになります。

しかしSSMLはただのXMLです。素のXMLを淡々と打ち込んでいくだけの精神力を、僕は持ち合わせていませんでした。

SSMLを気軽に作りたい -> Atomプラグイン作成

若干調べた感じ、フリーでいい感じにSSMLを編集させてくれるソフトは見当たらないようでした。 SSMLをもっと気軽にかけないと楽しい朗読までたどり着かない...ということで簡単なエディタを作ることにしました。

やりたいことはシンプル。タグを簡単に挿入していきたいのです。 また、使い方が簡単で、プログラマーでなくとも朗読用のSSMLを作れるようにしたい。

ということで、雑ですがイメージとしてはこんな感じです。 f:id:masashisalvador:20171222193402j:plain

しかしエディタを0から作るのはハードルが高い...ということで出来合いのものに乗っかることにします。 業務ではすっかりIntelliJの奴隷と化しているのですが、ここは一つATOM様に助けを求めたいとおもいます。

ATOMプラグイン作成自体は公式ドキュメント atom.io と幾つかの記事を参考にすると簡単に作ることができました。

以下、機能と書いたコードを少しだけ紹介します。

1. 休止の挿入

https://gyazo.com/7c4c3a8a92d6469b964be768d49adfd9

秒数を選択しておけば休止秒数が指定できるように、下記のようなコードを書いています。

  insertBreak() {
    let editor = atom.workspace.getActiveTextEditor();
    // 選択部分の取得
    let selectedText = editor.getSelectedText();
    var breakTag = "";

    if (selectedText.length <= 0) {
      breakTag = '<break strength="strong"/>';
    } else {
      breakTag = '<break time="' + selectedText + 's"/>'
    }

    editor.insertText(breakTag);
  },

2.強調の挿入

選択部分を強調します。選択部の冒頭にsr を付しておくと、強調タグ(emphasisタグ)のlevelが strongreduced になるようにしています。

https://gyazo.com/ad605e72ae354c9f5f7d41d32821452a

  insertEmphasis() {
    let editor = atom.workspace.getActiveTextEditor();
    let selectedText = editor.getSelectedText();
    if (selectedText.length <= 0) {
      return;
    }
  // 中略

    let tag = this.wrapTextWithEmphasisTagWithLevel(selectedText, level);
    editor.insertText(tag);
  },

まとめ

アドベントカレンダーまでには作り終わる!と意気込んでいたのですが、やっていくと色々と考慮しなければならない点が出てきたので未完の状態です。SSMLの各タグで指定できる属性が色々とあるので、属性をどうやってスムースに入力させるか...などを考えているとキリがなくなってきました。

ひとまず自分でひたすら使ってみて、駄目でも先に進むスタイルで年明けあたりに公開したいなと思っています。

それでは皆さんよいクリスマスを!

mattes / migrate のメモ

migrateの使い方

toolchain #golang

how to install? ref : https://github.com/mattes/migrate/tree/master/cli

# with homebrew
brew install migrate
# with curl
curl -L https://github.com/mattes/migrate/releases/download/v3.0.1/migrate.darwin-amd64.tar.gz
tar xvz migrate.darwin-amd64.tar.gz
mv migrate.darwin-amd64.tar.gz /usr/local/migrate

naming rule for migration file ref : https://github.com/mattes/migrate/blob/master/MIGRATIONS.md ここに書いてある形式でファイルを作成するらしい。 なんでupとdownを分けるの?についても

-database        Run migrations against this database (driver://url)
# DSNを指定すればいいっぽい?
# http://go-database-sql.org/accessing.html

up

[👾  @masashi.salvador.mitsuzawa] ~/dev/sample/mattes_migrate
% migrate -database 'mysql://root@tcp(127.0.0.1:3306)/hogehoge' -path ./ up
1/u create_hoge (15.876738ms)
[👾  @masashi.salvador.mitsuzawa] ~/dev/sample/mattes_migrate
% migrate -database 'mysql://root@tcp(127.0.0.1:3306)/hogehoge' -path ./ up
no change

down

[👾  @masashi.salvador.mitsuzawa] ~/dev/sample/mattes_migrate
% migrate -database 'mysql://root@tcp(127.0.0.1:3306)/hogehoge' -path ./ down
no change

良さそう。

実行すると schema_migrations テーブルが作成されます。

Firebase Authから返ってくるJWTをゴニョゴニョする (2)

前回の記事 allishackedoff.hatenablog.com の続きです。引き続きゴニョゴニョします。

JWTをDecodeする

golangのJWTのライブラリは github.com が最も使われているようです。 otiai10.hatenablog.com christina04.hatenablog.com この辺りでも利用されているようで、今回トライする際に参考にさせて頂きました。

Firebase Authから返ってくるJWTを検証する

検証するにはヘッダーをdecodeしないといけません。 headerに含まれる kid に対応するCERTIFICATEを使ってtokenが正しいかどうかを検証します。 「文字列としてcertificateを渡せばよしなにやってくれるやろ」とか甘いことを考えていましたが、しっかりrsa.PublicKey型のkeyを抜き出してやらないと verifyできません。

検証の流れは下記です。

  • JWTのヘッダーをDecode
  • "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com からcertificateのリストを取得
  • 取得したcertificateのリストから、 ヘッダーの kid に対応するkeyを見つける
    • 見つからない場合はexpireしている
  • 見つけたkeyをx509 パッケージを利用して rsa.PublicKey 型の変数にする
  • Tokenをdecode
  • decodeで生成されたjwt.Token Objectの Validを見る
    • expireしている / publickeyがうまく取得できていない場合は token.Valid == false になる

ソースコード (in Golang)

package main

import (
    "encoding/json"
    "encoding/pem"
    "errors"
    "fmt"
    "log"
    "net/http"
    "strings"

    "crypto/x509"

    "crypto/rsa"

    jwt "github.com/dgrijalva/jwt-go"
)

type header struct {
    Alg string `json:"alg"`
    Kid string `json:"kid"`
}

type payload struct {
    jwt.StandardClaims
}

func main() {
    const (
        publicKeySrcURL = "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com"
    )
    rawToken := "<PASTE YOUR Firebase Auth JWT"
    segs := strings.Split(rawToken, ".")

    bHeader, err := jwt.DecodeSegment(segs[0])
    if err != nil {
        log.Fatalln("error in decoding segment header")
    }

    var h header
    json.Unmarshal(bHeader, &h)

    resp, err := http.Get(publicKeySrcURL)
    if err != nil {
        log.Fatalln("error in getting public key")
    }
    defer resp.Body.Close()
        
       // 一応Streamで扱っておく
    decoder := json.NewDecoder(resp.Body)
    pubkeyMap := make(map[string]string)
    err = decoder.Decode(&pubkeyMap)
    if err != nil {
        fmt.Printf("%v", err)
        log.Fatalln("error on decoding http response body")
    }

    rawPubkey, ok := pubkeyMap[h.Kid]
    if !ok {
        log.Fatalln("public key corresponding to XXX is not found")
    }
    pk, err := parseKey([]byte(rawPubkey))
    if err != nil {
        panic(err)
    }

    token, err := jwt.Parse(rawToken, func(token *jwt.Token) (interface{}, error) {
        if _, ok = token.Method.(*jwt.SigningMethodRSA); !ok {
            return nil, errors.New("signing methods other than RSA are not expected")
        }
        if token.Method != jwt.SigningMethodRS256 {
            return nil, errors.New("Signing methods other than RS256 are not expected")
        }

        return pk, nil
    })

    //spew.Dump(token)
    if !token.Valid {
        fmt.Println("token is invalid")
    }
}

// --CETIFICATE -> rsa.PublicKeyへの変換
func parseKey(rawPubkey []byte) (interface{}, error) {
    block, _ := pem.Decode(rawPubkey)
    cert, _ := x509.ParseCertificate(block.Bytes)
    pk := cert.PublicKey.(*rsa.PublicKey)

    fmt.Println(pk)
    return pk, nil
}

string -> []byteへのキャストをやっているのがイケてないですが、とりあえずこれでdecodeできます。

NOTE

Certificate -> rsa.PublicKeyへの変換が所見だったのでこのあたりの知識を補強しておく

Firebase Authから返ってくるJWTをゴニョゴニョする(1)

同じモチベーションの人がいたら幸いです。

モチベーション

  • Rails にしろGoにしろ自前でAuth周りを書くのがだるい

    • ログイン機能は必要だけど、さっさと作って使われるか検証したい
    • 認証なしでやればいいじゃん?って話もあるけど、なんだかんだMyhogehogeみたいな機能を実装して試したい
  • Rails -> devisev? => なにか起こる度にRack::MiddleWareとかにログを指して折ってくのつらい..

    • 2010年くらい(@Masashisalvadorが学生だったころ)から同じような感じ..
  • Go

    • コードは読みに行けるけど..自前で実装すんのかよ => 本質的な機能の部分をさっさと実装したい (=認証は外付けにしたい)
  • FirebaseにAuthあるじゃん

    • 公式SDKはNodeとかか…
    • Googleがやってくれるならメンテしてくれるだろうし、無茶苦茶なことにはならないだろうからいいな
    • ID/Passwordログインも、各種サービスのOAuthに対応してる => 勝てる?
    • AuthはRest API経由だとできないのか => JWTを使えばまあ認証サーバの切り出し(=Firebaseにおまかせ)はできそうだな。
    • 3rd partyでホゲる際のことも公式ドキュメントに書いてあるし、いきなりAuthだけつかうんじゃねえ!とはならなそう。
    • ついでにPushとかもFirebase経由で送れるのは便利だ。

最終的にやりたいこと

  • Firebaseから飛んでくるJWTをよしなにパースして認証したい。
    • Rails or Goで(Goは技術調査も兼ねた自前プロジェクトで使う)
    • RailsだったらGem入れる or コードコピーしてrequireしてゴニョるくらいで使いたい = 使い回しを容易にしたい

とりあえず認証をザクっと書いてJWTの中身を覗いてみる

コードはクソ雑ですが…

onClickSignInWithFacbook() {
    firebase.auth().signInWithPopup(provider).then((result) => {
      // This gives you a Facebook Access Token. You can use it to access the Facebook API.
      global.rrr = result;
      let token = result.credential.accessToken;
      // The signed-in user info.
      let user = result.user;
      // ...3
      this.setState({ user });
      console.log(token);
      console.log(user);
    }).catch((error) => {
      console.log(error);
      console.log("Error on facebook auth");
      // Handle Errors here.
      let errorCode = error.code;
      let errorMessage = error.message;
      // The email of the user's account used.
      let email = error.email;
      // The firebase.auth.AuthCredential type that was used.
      let credential = error.credential;
      // ...
    });
  }
  onClickSignInWithGoogle() {
    firebase.auth().signInWithPopup(gProvider).then((result) => {
      // This gives you a Google Access Token. You can use it to access the Google API.
      global.rrr = result;
      let token = result.credential.accessToken;
      // The signed-in user info.
      let user = result.user;
      this.setState({ user });
  
      console.log(token);
      console.log(user);
      // ...
    }).catch((error) => {
      console.log("error on google auth");
      console.log(error);
      // Handle Errors here.
      let errorCode = error.code;
      let errorMessage = error.message;
      // The email of the user's account used.
      let email = error.email;
      // The firebase.auth.AuthCredential type that was used.
      let credential = error.credential;
      // ...
    });    
  }

Google Authの場合

rrr.credential.idToken # .でくぎられたJWTが入ってる
rrr.credential.idToken.split(".") # header / claims / signatureに別れてる

1つめのセグメント = splitした配列の先頭はただのヘッダのはず

echo -n "eyJhbGciOiJSUzI1NiIsImtpZCI6IjdhOWY5Yzc5ZGYzMDU5MzhlMjY4ODk3Y2JkNDc1ZDQ3MjY4MWI0ZWEifQ" | base64 -D                                                                                                                 
{"alg":"RS256","kid":"7a9f9c79df305938e268897cbd475d472681b4ea"%

JWTのヘッダが入っている。閉じカッコがないのが気になるがこれは? kidは公開鍵を探しに行く時に使う。

echo -n "2つ目のセグメント" | base64 -D | jq .
{
  "azp": "xxxxx.apps.googleusercontent.com",
  "aud": "xxxxx.apps.googleusercontent.com",
  "sub": "1111111111111111111",
  "email": "hoge@example.com",
  "email_verified": true,
  "at_hash": "xxxxxxx",
  "iss": "accounts.google.com",
  "iat": 1495084945,
  "exp": 1495088545
}

JWTのStandardぽいものに、認証に使ったemailなどが入っている。

Facebook Authの場合

上記コードの result.user.Yd というキーで JWTが入っていた。google Auth の場合も同様でした。(googleの場合はcredentialsにも入っている)

rrr.user.Yd.split(".")
{
  "iss": "https://securetoken.google.com/xxxx",
  "name": "Masashi Salvador Mitsuzawa",
  "picture": "https://scontent.xx.fbcdn.net/v/t1.0-1/p100x100/16142336_1426152514084193_2027921832216883945_n.jpg?oh=8d6238d7d50034e2fd7208c8a4208fc1&oe=59745742",
  "aud": "xxxx",
  "auth_time": 1495086800,
  "user_id": "xxxx",
  "sub": "xxxx",
  "iat": 1495086800,
  "exp": 1495090400,
  "email": "xxxxx@yahoo.co.jp",
  "email_verified": false,
  "firebase": {
    "identities": {
      "facebook.com": [
        "1562766520422791"
      ],
      "email": [
        "m_m_moonty@yahoo.co.jp"
      ]
    },
    "sign_in_provider": "facebook.com"
  }
}

こんな感じでClaimsがDecodeできる。

} (closing bracket)が消える問題

echo -n "eyJhbGciOiJSUzI1NiIsImtpZCI6IjdhOWY5Yzc5ZGYzMDU5MzhlMjY4ODk3Y2JkNDc1ZDQ3MjY4MWI0ZWEifQ" | base64 -D                                                                                                                 
{"alg":"RS256","kid":"7a9f9c79df305938e268897cbd475d472681b4ea"% # }が消えてる

同じ文字列を Golang で書いた適当なコードでdecodeしてやると

(string) (len=64) "{\"alg\":\"RS256\",\"kid\":\"e62eaba0e06fe1463746c69efa974c1152234634\"}"

問題なく閉じカッコが出現する

}エンコードすると

echo -n "}" | base64
fQ==

となるので、たしかに文字列としてはたりなさそうに見える。 Base64自体はRFCかなにかで規定されている(はず)なので、挙動差異は変なのだが…何なのだろう?