2017/01/11


おそらく golang を暫く使っておられる方であればご存じだと思いますが今日は crypto/ssh を紹介します。

Windows で ssh と聞くとどうしても msys やら cygwin やら入れないといけなくて

  • ランタイムを入れるのが嫌だ
  • 特殊なパス形式とか嫌だ
  • そもそも業務で使いづらい

といった個人的もしくは政治的な事柄が起きてなかなか実現しづらかったりします。でも golang なら msys や cygwin に頼らず ssh コマンドを、しかもライブラリとして扱う事が出来るので golang で作ったウェブサーバやバッチから UNIX ホストに対して ssh コマンドを送る事が出来るのです。

ssh - GoDoc

package ssh import "golang.org/x/crypto/ssh" Package ssh implements an SSH client and server. SSH is...

https://godoc.org/golang.org/x/crypto/ssh

しかも openssh に依存していないので、openssh の実装に脆弱性が発見されたとしても影響を受けません。インタフェースも netos/exec がうまく組み合わさったイメージで扱う事が出来て非常に便利かつ拡張性のあるパッケージになっています。どれくらい簡単で拡張性が高いかを分かって頂ける様にオレオレ ssh コマンドを作ってみました。通常 ssh コマンドはユーザ名、ホストおよびオプションを指定して ssh コマンドを起動し、パスワードプロンプトにパスワード(パスフレーズ)を入力してログインしますが、この例ではパスワードをコマンド引数から得られる様にしてあります。

package main

import (
    "flag"
    "fmt"
    "os"
    "strings"
    "time"

    "golang.org/x/crypto/ssh"
)

var (
    user     = flag.String("u""""user")
    password = flag.String("p""""password")
    port     = flag.Int("P"22"port")
)

func run() int {
    flag.Parse()
    if flag.NArg() == 0 {
        flag.Usage()
        return 2
    }

    config := &ssh.ClientConfig{
        User: *user,
        Auth: []ssh.AuthMethod{
            ssh.Password(*password),
        },
        Timeout: 5 * time.Second,
    }

    hostport := fmt.Sprintf("%s:%d", flag.Arg(0), *port)
    conn, err := ssh.Dial("tcp", hostport, config)
    if err != nil {
        fmt.Fprintf(os.Stderr, "cannot connect %v%v", hostport, err)
        return 1
    }
    defer conn.Close()

    session, err := conn.NewSession()
    if err != nil {
        fmt.Fprintf(os.Stderr, "cannot open new session: %v", err)
        return 1
    }
    defer session.Close()

    go func() {
        time.Sleep(5 * time.Second)
        conn.Close()
    }()

    session.Stdout = os.Stdout
    session.Stderr = os.Stderr
    session.Stdin = os.Stdin
    err = session.Run(strings.Join(flag.Args()[1:], " "))
    if err != nil {
        fmt.Fprint(os.Stderr, err)
        if ee, ok := err.(*ssh.ExitError); ok {
            return ee.ExitStatus()
        }
        return 1
    }
    return 0
}

func main() {
    os.Exit(run())
}

os/exec.Command と同じ様に os.Stdout や os.Stderr をパイプ出来る様になっていて、終了コードも得られる様になっています。簡単ですね。もう少しコードを足せば公開鍵認証を行う事も出来ます。詳しくはドキュメントを参照して下さい。サーバがパスワード認証をサポートしている場合にはバッチコマンドとして実行出来るので、もしかすると意外と便利かもしれません。ただしパスワードがコマンド引数になるという事は、ps コマンドで他のユーザにパスワードが漏れてしまう危険性がある事を理解しておいて下さい。

みんなのGo言語【現場で使える実践テクニック】 みんなのGo言語【現場で使える実践テクニック】
松木雅幸, mattn, 藤原俊一郎, 中島大一, 牧 大輔, 鈴木健太
技術評論社 / ¥ 2,138 (2016-09-09)
 
発送可能時間:在庫あり。


2017/01/06


Twitter で「言語のしくみ」読みたいなって呟いたら Matz 本人から「献本しましょうか」とメンション頂いて即答でお願いしました。ありがとうございます。

まつもとゆきひろ 言語のしくみ まつもとゆきひろ 言語のしくみ
まつもとゆきひろ
日経BP社 / (2016-12-22)
 
発送可能時間:

ひさびさ紙の本を通勤電車の中で立ちながら読んだので手がだるくなりました。なんだか懐かしい感じがしました。

さてこの本ですが、一言で言うとこんな本です。

Ruby のパパこと Matz が雑誌の連載に追われながら試行錯誤して作ったプログラミング言語「Streem」を解説する本

聞こえが悪かったらすみません。言いたいのはこの「試行錯誤」がとても良いエッセンスになっている点なのです。実際にはその連載記事をまとめた物に対して、この当時はこの様に考えていたが後になってみると実は良く無かったといった振り返り「タイムマシンコラム」で構成されています。

この連載が1つの本に纏められた事でプログラミング言語設計者の葛藤が非常に良く表されているな、そう思ったのです。本書の中では Matz が「以降 Streem をこういう設計にしたい」と考え実装していくのですが、いくら Matz とは言えど設計に迷いは生じます。連載の為に期間も守らないといけません。どっちが良いか迷って実装してしまった後、次月号で別の実装に直してしまうといった内容も出てきます。一エンジニアとしてすごく共感できました。はじめから出来上がった Streem に対して解説して行く内容になっていたら、もしかするとこんなに面白くなっていなかったかもしれません。

内容は Streem を設計(デザイン)し実装していく過程を細かく書いてあり、プログラミング言語を作る過程に必要な知識や歴史、なぜあの言語はそういうデザインになっているか、なぜ Ruby はこうなっているのか、そういった Matz でしか知り得ない内容も盛りだくさんです。プログラミング言語に興味のある方は買いではないでしょうか。

ところで皆さん、プログラミング言語って作った事あるでしょうか。プログラミング言語の実装は、一般的なライブラリやツール類と比べると設計や実装内容が幾らか異なります。正直、技術スキルも要します。ある程度動く様な形に持っていくには何度も心が折れそうにもなります。インターネットのブログ記事で「○○言語を作った」といった内容を読んで

なんとなく理解はできる。僕もやればきっと出来る。でも時間がない。

そう考えている人もいるかもしれません。僕も大昔はそう思っていたかもしれません。ただ幾らかプログラミング言語を作ったり、色んなプログラミング言語にコントリビュートしてきた経験のある僕からひとこと言わせて貰えるならば、こう言いたのです。

プログラミング言語はある程度の自信がないと作れないが、作れるとそれはより大きな、そしてより確かな自信へと変わる

ちょっとカッコ良すぎる事を書いてしまいましたが、実際にそうなのです。人生の中でプログラミング言語を作るなんて事は何十回もやれる経験では無いです。だからこそ自分が手掛けたプログラミング言語が一様に動く様になった際には、なんとも言えない愛着が湧いてくるのです。そして至らない設計を見つけた際にはとにかく憎たらしくもなります。自分で作ったプログラミング言語と葛藤する事になります。「なんでこんな設計にしちゃったんだよ」と自分を戒める事も多々起きます。でもそれを乗り越えた時の満足感は、実装してみた事がある人しか分からない自信へと変わるのです。

Matz も本書で書かれていましたが、プログラミング言語をデザインしている時はよく手が止まります。僕のケースであれば、よくブツブツと喋っているそうです。プログラミング言語を作るにはコンピュータを触っていない時間の方が大事だと僕は思います。あーでもないこーでもないと頭を捻って編み出した最適化がうまくキマった時には誰かに言いたいけど誰にもいえない変な喜びが起きます。プログラミング言語を作った事がある人ならば分かって貰えると思います。一見一人でブツブツ言ってニヤニヤしてとても変態っぽいのですが、そんな僕から見ても Matz の言語オタク度は手の届かない領域だと分かる一冊でした。読みながら「ほんとに言語好きなんだなw」と言いたくなりました。プログラミング言語に係った事がある人ならば、とにかく読みながら「うんうん、分かる分かる」と言いたくなる本です。その中でも特に「わかるー」と言いたくなったのが以下の一言です。

開発者・設計者が使いたい言語そして機能でないとユーザは使ってくれない

手前味噌の話ですが、Vim script には lambda が無くとても不便でした。リストを並べ替える sort に比較関数を渡す為だけに名前付き関数を定義しないといけませんでした。とにかく欲しくて何度か vim-dev に駆け寄りました。メンションは貰えるのですが、結局だれも作り出そうとしないまま月日が流れて諦めかけていたのですが

誰もやってくれないなら自分でやるしかない。だって世界中でこの機能が一番欲しいのは僕なんだ。

と心に決め、思うがままに実装しました。最終的には形はずいぶんと変わってしまいましたが、vim8 に lambda が入った際にはなんとも言えない満足感が得られました。

lambda 関数作ろうぜ! · Issue #632 - vim-jp/issues - GitHub

キャプチャされる関数の引数名がキャプチャする関数の引数名とバッティングすることは通常どの言語でもあり得ますし、一般的にそういう場合は一時変数で別名に預けるか、JavaScript の様に即時関数を作っ...

https://github.com/vim-jp/issues/issues/632

やって良かった、そう思います。

さて話を戻します。冒頭にも書きましたが、本書には「なぜ Ruby はこうなっているのか」が何度も登場します。特に僕が面白いなと思ったのは「Ruby のブロックの設計と、作者が意図していなかった使われ方」の話です。Matz の考えたブロックはイテレーションを実現する為の物でしたが、実際にはイベントハンドラであったり、コールバックであったり、select の様な条件記述であったり、スレッドや fork のインスタンスであったり、時には設定ファイルの DSL だったりもします。これらはユーザが考えた使い方です。作者が意図していなかった設計が違う方向で広がっていったという話はとても面白かったです。僕も libuv の mruby binding である mruby-uv でイベントハンドラやコールバックとして使わせて貰っています。

GitHub - mattn/mruby-uv: interface to libuv for mruby(experimental)

README.md mruby-uv interface to libuv for mruby(experimental) License MIT libuv Current mruby-uv use...

https://github.com/mattn/mruby-uv

もう1点、面白かった内容として上げたいのが「言語設計者のマーケティング論」です。どうやったら人気が出るのかを Ruby の作者である Matz が思いを喋っているのは、とても興味深い物でした。せっかく苦労して作った物なので、誰かの目に止まって欲しいですよね。

ところで本書には僕の名前が何度か登場します。Matz が Streem のリポジトリを公開し、まだコードも無いのに Hacker News で取り上げられ、話題性が落ち着いてきたそんな頃、僕はある pull-request を送りました。

WIP: tiny VM by mattn - Pull Request #101 - matz/streem - GitHub

You signed in with another tab or window. Reload to refresh your session. You signed out in another ...

https://github.com/matz/streem/pull/101

これまで BNF のシンタックスチェッカーでしか無かった Streem に処理系のコードを足しました。当時は Matz にもいろんなカンファレンスでスライドのネタとして扱って頂き見ていてとても楽しかったのを覚えています。ただし本書でも書かれていますが、僕がやったのは継続的なコントリビュートでは無いのです。OSS を運営する上で本当に必要なのは、継続的に手を動かしてくれるコントリビュータ達です。一人の手で出来上がったプロダクトには、作者の考えしか入り込まないので「本当に皆が欲しいと思っている機能」は出て来ない事があります。こういった OSS とコントリビュートに関する話が Ruby の生みの親の言葉として読める点も、本書の楽しみ方の一つだと思います。

この本があれば、「プログラミング言語が作れるかも」そんな気持ちになると思います。ぜひ自分の言語を作ってみて欲しいと思います。

そして迷って躓いて悩んで乗り越えた時に得られる満足感を、ぜひ体験してみて欲しいと思います。


2016/12/31


OAuth2 でレスポンスタイプがコードもしくはトークンの場合、ブラウザで認証を行ってコードやトークンを自前サーバで受け取る事になる。モバイルアプリだと組み込みブラウザが前提になっておりリダイレクトの最終 URL からアクセスコードやトークンを得る。ただコマンドラインアプリの場合、認証の為に起動したブラウザの最終 URL を得る方法はない。また1コマンドラインアプリケーションの為にドメイン付きのコールバックサーバを用意するのも面倒だし、作ったサーバをユーザに信用して貰う必要がある。あとそもそも外部のサーバで受け取ったトークンをどうやってコマンドラインアプリに渡すかという問題がある。

そこで使うのがローカルサーバを立てる方法。認証後のコールバック先をコマンドラインアプリから起動したローカルサーバにし、そこにリダイレクトさせてアクセストークンを貰い保存する。

今日はこれが伝わり易い用に Microsoft の OneNote API で認証するサンプルを書いてみる。

まずは Microsoft Application Registration Portal でアプリケーションを登録する。

マイ アプリケーション

Microsoft developer Windows Windows Dev Center Windows apps Desktop Internet of Things Games Hologra...

https://apps.dev.microsoft.com/#/appList

アプリケーションシークレットでパスワードを発行しそれを ClientSecret とすること。ちなみに ClientID はアプリケーションIDと一緒。またプラットフォームとしては Web アプリケーションを選びコールバックサーバ(Microsoft の API は省略不可)の URL として http://localhost:8989 を指定しておくこと。

本来ならばコールバック先がアプリケーション側から指定できるため、Golang であれば以下の oauth2.Config にて RedirectURL を自由に決められる。

oauthConfig := &oauth2.Config{
    Scopes: []string{
        "wl.signin",
        "wl.basic",
        "Office.onenote",
        "Office.onenote_update",
        "Office.onenote_update_by_app",
    },
    Endpoint: oauth2.Endpoint{
        AuthURL:  "https://login.live.com/oauth20_authorize.srf",
        TokenURL: "https://login.live.com/oauth20_token.srf",
    },
    ClientID:     config["ClientID"],
    ClientSecret: config["ClientSecret"],
    RedirectURL:  "http://localhost:8989",
}
なので例えば、空いているポートを使ってローカルサーバを立ち上げる事ができる。
l, err := net.Listen("tcp""localhost:0")
if err != nil {
    return "", err
}
defer l.Close()
/* l.Attr().String() がサーバのアドレスになる */

ここで http.ListenAndServe でサーバを起動するのではなく、net.Listen で作ったリスナでサーバを起動しているのは、アクセストークンを受け取った後にサーバを停止しないといけないからです。go1.8 では graceful shutdown がサポートされているが、まだ go1.8 はリリースされていない。次に以下の様にお決まりの手順で認証 URL へアクセスする。

stateBytes := make([]byte16)
_, err = rand.Read(stateBytes)
if err != nil {
    return "", err
}
state := fmt.Sprintf("%x", stateBytes)
err = open.Start(oauthConfig.AuthCodeURL(state, oauth2.SetAuthURLParam("response_type""token")))
if err != nil {
    return "", err
}

ここで Microsoft の認証方式だとクエリ文字列ではなく、URL フラグメントとして帰ってくるのでサーバが直接アクセストークンを読み取れない。よって以下のハックを使ってフラグメントをクエリ文字列に変えて自分にループバックさせる。

quit := make(chan string)
go http.Serve(l, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
    if req.URL.Path == "/" {
        w.Write([]byte(`<script>location.href = "/close?" + location.hash.substring(1);</script>`))
    } else {
        w.Write([]byte(`<script>window.open("about:blank","_self").close()</script>`))
        w.(http.Flusher).Flush()
        quit <- req.URL.Query().Get("access_token")
    }
}))

/ でリクエストされるとフラグメント文字列をクエリ文字列に変えてアクセスし直す。そしてそのハンドラで access_token を取得して、ブラウザは自らのタブを消滅させる。なのでコマンドラインアプリのユーザは勝手に起動したブラウザでログインすればいいだけになる。全体のコードはおおよそ以下の様になる。

ただし Microsoft の API アクセストークンは発行後1時間で揮発するのでリフレッシュトークン等の処理は必要。

package main

import (
    "crypto/rand"
    "encoding/json"
    "fmt"
    "io"
    "io/ioutil"
    "log"
    "net"
    "net/http"
    "os"
    "path/filepath"
    "runtime"
    "time"

    "golang.org/x/oauth2"

    "github.com/skratchdot/open-golang/open"
)

type Page struct {
    Title          string `json:"title"`
    CreatedByAppID string `json:"createdByAppId"`
    Links          struct {
        OneNoteClientURL struct {
            Href string `json:"href"`
        } `json:"oneNoteClientUrl"`
        OneNoteWebURL struct {
            Href string `json:"href"`
        } `json:"oneNoteWebUrl"`
    } `json:"links"`
    ContentURL                string    `json:"contentUrl"`
    LastModifiedTime          time.Time `json:"lastModifiedTime"`
    CreatedTime               time.Time `json:"createdTime"`
    ID                        string    `json:"id"`
    Self                      string    `json:"self"`
    ParentSectionOdataContext string    `json:"parentSection@odata.context"`
    ParentSection             struct {
        ID   string `json:"id"`
        Name string `json:"name"`
        Self string `json:"self"`
    } `json:"parentSection"`
}

func get(url, token string, val interface{}) error {
    req, err := http.NewRequest("GET", url, nil)
    if err != nil {
        return err
    }
    req.Header.Add("Authorization""Bearer "+token)
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    if val != nil {
        r := io.TeeReader(resp.Body, os.Stdout)
        return json.NewDecoder(r).Decode(val)
    }
    _, err = io.Copy(os.Stdout, resp.Body)
    return err
}

func getConfig() (stringmap[string]stringerror) {
    dir := os.Getenv("HOME")
    if dir == "" && runtime.GOOS == "windows" {
        dir = os.Getenv("APPDATA")
        if dir == "" {
            dir = filepath.Join(os.Getenv("USERPROFILE"), "Application Data""onenote")
        }
        dir = filepath.Join(dir, "onenote")
    } else {
        dir = filepath.Join(dir, ".config""onenote")
    }
    if err := os.MkdirAll(dir, 0700); err != nil {
        return ""nil, err
    }
    file := filepath.Join(dir, "settings.json")
    config := map[string]string{}

    b, err := ioutil.ReadFile(file)
    if err != nil && !os.IsNotExist(err) {
        return ""nil, err
    }
    if err != nil {
        config["ClientID"= "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
        config["ClientSecret"= "XXXXXXXXXXXXXXXXXXXXXXX"
    } else {
        err = json.Unmarshal(b, &config)
        if err != nil {
            return ""nil, fmt.Errorf("could not unmarshal %v%v", file, err)
        }
    }
    return file, config, nil
}

func getAccessToken(config map[string]string) (stringerror) {
    l, err := net.Listen("tcp""localhost:8989")
    if err != nil {
        return "", err
    }
    defer l.Close()

    oauthConfig := &oauth2.Config{
        Scopes: []string{
            "wl.signin",
            "wl.basic",
            "Office.onenote",
            "Office.onenote_update",
            "Office.onenote_update_by_app",
        },
        Endpoint: oauth2.Endpoint{
            AuthURL:  "https://login.live.com/oauth20_authorize.srf",
            TokenURL: "https://login.live.com/oauth20_token.srf",
        },
        ClientID:     config["ClientID"],
        ClientSecret: config["ClientSecret"],
        RedirectURL:  "http://localhost:8989",
    }

    stateBytes := make([]byte16)
    _, err = rand.Read(stateBytes)
    if err != nil {
        return "", err
    }

    state := fmt.Sprintf("%x", stateBytes)
    err = open.Start(oauthConfig.AuthCodeURL(state, oauth2.SetAuthURLParam("response_type""token")))
    if err != nil {
        return "", err
    }

    quit := make(chan string)
    go http.Serve(l, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
        if req.URL.Path == "/" {
            w.Write([]byte(`<script>location.href = "/close?" + location.hash.substring(1);</script>`))
        } else {
            w.Write([]byte(`<script>window.open("about:blank","_self").close()</script>`))
            w.(http.Flusher).Flush()
            quit <- req.URL.Query().Get("access_token")
        }
    }))

    return <-quit, nil
}

func main() {
    file, config, err := getConfig()
    if err != nil {
        log.Fatal("failed to get configuration:", err)
    }
    if config["AccessToken"== "" {
        token, err := getAccessToken(config)
        if err != nil {
            log.Fatal("faild to get access token:", err)
        }
        config["AccessToken"= token
        b, err := json.MarshalIndent(config, """  ")
        if err != nil {
            log.Fatal("failed to store file:", err)
        }
        err = ioutil.WriteFile(file, b, 0700)
        if err != nil {
            log.Fatal("failed to store file:", err)
        }
    }

    var pages struct {
        Value []Page `json:"value"`
    }
    err = get("https://www.onenote.com/api/v1.0/me/notes/pages", config["AccessToken"], &pages)
    if err != nil {
        log.Fatal(err)
    }
    for _, item := range pages.Value {
        err = get("https://www.onenote.com/api/v1.0/me/notes/pages/"+item.ID+"/preview?includeIDs=true", config["AccessToken"], nil)
        if err != nil {
            log.Fatal(err)
        }
    }
}