2018年11月6日火曜日

Cytoscape.js を使ってグラフを描画してみた

概要

Cytoscape.js は Cytoscape プロジェクトから派生した JavaScript 用のライブラリです
ノードとエッジを使ったグラフを描画することができます
今回は Getting Started 的な内容を紹介します

環境

  • macOS 10.14
  • Chrome 70.0.3538.77
  • Cytoscape.js 3.2.19

index.html

  • vim index.html
<html>
  <head>
    <script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.2.19/cytoscape.js"></script>
    <link rel="stylesheet" type="text/css" href="custom.css">
  </head>
  <body>
    <div id="cy"></div>.
    <script src="custom.js"></script>
  </body>
</html>

まず Cytoscape.js の読み込みを行います
cdnjs があるのでそれを使わせてもらいます
jQuery 構文を使いたいので jQuery も読み込んでいます
あとはこの後作成するグラフ描画用の js ファイルとデザインを当て込む css を読み込んでいます

div タグでグラフを描画する領域を定義します
この領域を指定してグラフを描画するために id="cy" を指定しておきましょう
またポイントとしては div タブが描画されたあとに custom.js (グラフを描画するようのファイル) を読み込む必要があります
そうしないと Uncaught TypeError: Cannot read property 'indexOf' of undefined のエラーが発生してしまいます

custom.css

  • vim custom.css
#cy {
  width: 100%;
  height: 100%;
  display: block;
}

div タグの領域の広さを定義しておきます
とりあえず今回は画面いっぱいに出力します

custom.js

グラフを描画するメインの処理になります

  • vim custom.js
var cy = cytoscape({
  container: $('#cy'),
  elements: [
    {
      data: { id: 'a' }
    },
    {
      data: { id: 'b' }
    },
    {
      data: { id: 'ab', source: 'a', target: 'b' }
    }
  ],
  style: [
    {
      selector: 'node',
      style: {
        'background-color': '#666',
        'label': 'data(id)'
      }
    },
    {
      selector: 'edge',
      style: {
        'width': 3,
        'line-color': '#ccc',
        'target-arrow-color': '#ccc',
        'target-arrow-shape': 'triangle'
      }
    }
  ],
  layout: {
    name: 'grid',
    rows: 1
  }
});

まず container: $('#cy') でグラフを描画する領域を指定します
そして elements, style, layout を使ってノードとエッジの定義および描画を行います

まず elements ですがここで描画するノードとエッジを定義します

elements: [
  {
    data: { id: 'a' }
  },
  {
    data: { id: 'b' }
  },
  {
    data: { id: 'ab', source: 'a', target: 'b' }
  }
]

今回はノードが 2 つ、エッジを 1 つ定義します
source と target が指定されている場合はエッジになります

次に style です

style: [
  {
    selector: 'node',
    style: {
      'background-color': '#666',
      'label': 'data(id)'
    }
  },
  {
    selector: 'edge',
    style: {
      'width': 3,
      'line-color': '#ccc',
      'target-arrow-color': '#ccc',
      'target-arrow-shape': 'triangle'
    }
  }
]

elements で定義した node と edge に対するスタイルを定義します
背景色やラベル、線の太さなどを指定します

最後に layout です

layout: {
  name: 'grid',
  rows: 1
}

rows は初期ノードを表示する際の行数を指定できます
例えば 1 にした場合は行数は 1 行になるのでノードが横に並べられて表示されます
2 にすると 1 画面に 2 行 (ノード) 配置することが可能になるので今回のサンプルであれば縦に表示されます

縦にたくさんのノードを並べたいのであれば rows の値を大きくすると良いと思います

動作確認

  • open index.html

で以下のように表示されると思います
cytoscape1.png

ノードは好きな箇所に移動できます
またエッジを掴むとグラフ全体を移動することができます

最後に

Cytoscape.js を使って簡単なノードとエッジのグラフを描画してみました
シンプルで良いかなと思います
デモもたくさん用意されています

基本はネットワークグラフを書くためのライブラリなのでチャートなどのグラフを書きたい場合は d3.js などを使うと良いと思います

参考サイト

2018年11月2日金曜日

CentOS7 に Ruby2.5 をインストールする方法

概要

CentOS7 に最新版の Ruby をインストールする方法を紹介します
base レポジトリを使うと未だに 2.0 しかインストールできません

環境

  • CentOS 7.5.1804
  • Ruby 2.5.0p0

結論

Red Hat Software Collections (RHSCL) を使う
RVM や rbenv を使う方法が紹介されていますが面倒です

方法 (コマンド)

  • yum install centos-release-scl
  • yum -y install rh-ruby25 rh-ruby25-ruby-devel

これでインストールは完了です
このままだと libruby.so.2.5: cannot open shared object file: No such file or directory という感じで共有ライブラリが見えない場所にあるので追加してあげます

  • vim /etc/ld.so.conf.d/rh-ruby.conf
/opt/rh/rh-ruby25/root/usr/lib64
  • ldconfig

これで OK です
あとはお好みに合わせて PATH に追加してあげましょう

  • /opt/rh/rh-ruby25/root/usr/bin/ruby -v
  • /opt/rh/rh-ruby25/root/usr/bin/gem install bundler
  • /opt/rh/rh-ruby25/root/usr/local/bin/bundle -v
  • echo 'export PATH=$PATH:/opt/rh/rh-ruby25/root/usr/bin:/opt/rh/rh-ruby25/root/usr/local/bin' >> ~/.bashrc

2018年10月30日火曜日

Twitter の OAuth を使ってログイン機能を作ってみた

概要

Twitter の OAuth 機能を使って Web アプリにログイン機能を付けてみました
アプリは Sinatra で作成します

環境

  • macOS 10.14
  • Ruby 2.5.1p57
    • oauth 0.5.4
    • twitter 6.2.0

Twitter アプリの作成

https://developer.twitter.com/en/apps から作成します
アプリを作成するのにいろいろと情報を登録しなければいけなくなったので頑張って登録します
App Details はこんな感じです
ポイントはコールバック用の URL で http://localhost:9292/redirect に設定しあとでアプリ側で実装します

twitter_oauth01.png

Permissions は Read and Write にしました
ログインだけであれば Read だけで OK です
twitter_oauth02.png

アプリを作成したら Keys and tokens から Consumer API Keys の API key と API secret key をメモしておきます

ライブラリインストール

  • bundle init
  • vim Gemfile
gem "oauth"
gem "sinatra"
gem "twitter"
  • bundle install --path vendor

oauth でログインしたあとに Twitter API をコールするためのアクセストークンを使って API をコールします

サンプルアプリ

  • vim config.ru
require './app'
run TwitterOAuth
  • vim app.rb
require 'sinatra'
require 'oauth'
require 'twitter'

class TwitterOAuth < Sinatra::Base
  TWITTER_API_KEY = 'api-key'
  TWITTER_API_SECRET = 'api-secret'
  TWITTER_CALLBACK = 'http://localhost:9292/redirect'

  enable :sessions

  helpers do
    def oauth
      OAuth::Consumer.new(
        TWITTER_API_KEY,
        TWITTER_API_SECRET,
        :site => 'https://api.twitter.com',
        :schema => :header,
        :method => :post,
        :request_token_path => '/oauth/request_token',
        :access_token_path => '/oauth/access_token',
        :authorize_path => '/oauth/authorize'
      )
    end
  end

  get '/' do
    request_token = oauth.get_request_token(:oauth_callback => TWITTER_CALLBACK)
    session[:token] = request_token.token
    session[:secret] = request_token.secret
    redirect request_token.authorize_url
  end

  get '/redirect' do
    oauth_client = oauth
    request_token = OAuth::RequestToken.new(oauth_client, session[:token], session[:secret])
    access_token = oauth_client.get_access_token(request_token, :oauth_verifier => params[:oauth_verifier])
    # access_token.params['screen_name']
    twitter_client = Twitter::REST::Client.new do |config|
      config.consumer_key = TWITTER_API_KEY
      config.consumer_secret = TWITTER_API_SECRET
      config.access_token = access_token.token
      config.access_token_secret = access_token.secret
    end
    "Logged in #{twitter_client.user.name}"
  end
end

説明

まず / にアクセスすると oauth.get_request_token を使ってログイン認証用の URL を取得し認証画面にリダイレクトされます

そこで Twitter 認証すると /redirect に飛ばされ認証時に渡される :oauth_verifier を使って oauth_client.get_access_token でトークンを取得します
あとは Twitter API のクライアントを使ってユーザ情報を取得しています

動作確認

ログインしていないと初めは認証画面が表示されます
認証に成功するとアプリからのアクセスを許可するか確認する画面になるので許可します
twitter_oauth3.png

あとは localhost に戻ってきて Twitter API がコールされユーザ名がブラウザに表示されると思います

最後に

Twitter の OAuth を使って Web アプリのログイン機能を実装してみました
今回は oauth というライブラリを使いましたが Twitter の場合、使えるライブラリは他にもいろいろあるようです (omniauth-twitter, Twitter ライブラリを使ってもできるようです)
この辺りは環境に合わせて好きなものを選択すれば良いと思います

Twitter の場合アクセストークンにシークレットもあるのが他の OAuth と少し違うところかなと思います
また権限も API 単位というわけではなく大雑把に Read or Write という感じで設定するのも他と異なる点かなと思います

2018年10月29日月曜日

Google の OAuth 認証を使ってみた

概要

Google の OAuth2 認証を Ruby アプリを使って実装してみました
アカウントの作り方が少しクセがあるようです

環境

  • macOS 10.14
  • Ruby 2.5.1p57
  • Google OAuth2 (2018/10/29 時点)
  • Google Drive API (2018/10/29 時点)

事前作業

GCP でプロジェクトを作成しておきましょう

Google Drive API の有効化

今回は認証後に Google Drive API をコールしてドライブ内にあるファイルの一覧を取得します
プロジェクトが Google Drive API を有効な状態にしておく必要があるので有効にします
google_oauth6.png

OAuth 認証画面の設定

これを設定しないとクライアント ID を作成できないので先に作成しておきます
設定するのはアプリケーション名だけで OK です
google_oauth2.png

クライアント ID の作成

OAuth 用のクライアント ID を作成します
作成の導線はたくさんあるのでお好きな方法で作成してください
google_oauth1.png

GCP では用途に合わせていろいろな認証情報が作成できます
今回は OAuth なので「OAuth クライアント ID」を選択します

クライアントのタイプを選択する必要があるので「ウェブアプリケーション」を選択しましょう
google_oauth3.png

あと大事なのはコールバック用の URL です
今回は localhost で開発するので http://localhost:9292/redirect と入力しましょう
あとでこのルーティングをアプリ上で実装します

クライアント ID を作成すると client_secret_279273981030-rj9ivjoip1ocvs3f5bh2ldoavk6erums.apps.googleusercontent.com.json というような名前の認証用の JSON ファイルをダウンロードできるようになります
ダウンロードして名前を client_secrets.json に変更しましょう

ライブラリインストール

それではアプリを作成します
まずはライブラリをインストールします

  • bundle init
  • vim Gemfile
gem "sinatra"
gem "google-api-client"
  • bundle install --path vendor

サンプルアプリ

  • vim config.ru
require './app'
run TestWebApp
  • vim app.rb
require 'sinatra/base'
require 'google/api_client/client_secrets'
require 'google/apis/drive_v2'
require 'json'

module Rack
  class Lint
    def call(env = nil)
      @app.call(env)
    end
  end
end

class TestWebApp < Sinatra::Base

  before do
    client_secrets = Google::APIClient::ClientSecrets.load
    @auth_client = client_secrets.to_authorization
  end

  get '/' do
    @auth_client.update!(
      :redirect_uri => 'http://localhost:9292/redirect',
      :scope => 'https://www.googleapis.com/auth/drive.metadata.readonly'
    )
    auth_uri = @auth_client.authorization_uri.to_s
    redirect auth_uri
  end

  get '/redirect' do
    @auth_client.code = request['code']
    @auth_client.fetch_access_token!
    auth_client = Signet::OAuth2::Client.new(JSON.parse(@auth_client.to_json))
    drive = Google::Apis::DriveV2::DriveService.new
    files = drive.list_files(options: { authorization: auth_client })
    "<pre>#{JSON.pretty_generate(files.to_h)}</pre>"
  end
end

説明

ポイントを説明します
before で client_secrets = Google::APIClient::ClientSecrets.load とすることでダウンロードして JSON ファイルを読み込みクライント ID とシークレットを設定します
更にそこからトークンを取得するための @auth_client を作成しておきましょう
今回は before でやっていますがリクエストごとに毎回やる必要はないです

@auto_client.update! で redirect_url と scope を設定します
redirect_url は先程クライアント ID 作成時に設定した http://localhost:9292/redirect を設定します
また scope はこのアプリが使用する API を指定します
今回は Google Drive API を使ってドライブの情報を確認するので https://www.googleapis.com/auth/drive.metadata.readonly を指定します
スコープの一覧はこちらを参考にしてください

redirect_uri と scope を指定したら @auth_client.authorization_uri.to_s で認証ページの URI を取得します
あとはこれにリダイレクトします

認証が成功したあとは /redirect に戻ってきます
戻ってきたら code を取得します
これを使って更にリクエストすることで token を取得します
あとは Drive API ようのクライアントを作成して drive.list_files でドライブ内のファイルの一覧を取得しています

動作確認

  • bundle exec rackup config.ru

localhost:9292 にアクセスすると認証画面にリダイレクトされます
google_oauth4.png

認証が成功すると Google Drive API にアクセスしても良いか確認する画面になるので許可を選択します
google_oauth5.png

すると Google Drive API から取得した JSON の生データがブラウザで確認できると思います
あとはお好きなように HTML なりを組み立てれば独自の Google Drive ブラウザができます

最後に

Google の OAuth を使ってみました
基本は OAuth なのでそれにそってクライアントを使うだけです

ポイントになりそうなのはスコープの指定方法かなと思います
基本は URL で指定する必要があります
あとは大量にスコープがあるのでどの API がどのスコープに紐付いているかわかりづらいです
また取得したい情報がどのスコープを有効にすることで取得できるのかもわからいづらいかなと思います
その辺りは各種 Google の API ドキュメントを見るしかないかなと思います

参考サイト

2018年10月27日土曜日

Let's Encrypt で manual プラグインで作成した証明書を更新する方法

概要

証明書を一番初めに作成した際に --manual オプションを付与した場合の更新方法です
やり方は同じように証明書を取得するだけです

更新

  • sudo certbot certonly --manual --preferred-challenges=dns -d your.domain.com

--preferred-challenges=dns は DNS チャレンジの場合に付与してください
あとは指定の TXT レコードを追加するだけです

  • dig -t txt _acme-challenge.your.domain.com

でレコードが引けるまで次に進まないように注意してください

最後に

--manual + DNS チャレンジで自動更新する方法はあるのだろうか
もしやるとしたら certbot-external-auth:out-handler を使って DNS の API を使って TXT レコードを登録するスクリプトを作る感じになると思います

2018年10月26日金曜日

Google Map Platform を使ってみた

概要

Google Map API は Google Map Platform として生まれ変わりました
API も整理され用途に応じてプラットフォームが用意されています
今回は各プラットフォームで用意されている API を試してみました
無料枠でできる範囲ですが Google Map Platform は有料のサービスになっているのでご注意ください

環境

  • macOS 10.14
  • Google Map Platform (2018/10/25)

Google Map Platform の有効化

ここから登録します
Google Map Platform には「Maps」「Routes」「Places」という 3 つの機能に分類されています
とりあえずすべての API を有効化しましょう
google_map_platform1.png

次にプロジェクトを選択します
無料枠はありますが Google Map Platform は有料サービスなのでプロジェクトも請求可能なプロジェクトを選択しましょう
google_map_platform2.png

API が有効化するとキーが発行されるのでメモしておきましょう
google_map_platform3.png

Maps Javascript API を使ってみる

Maps の Javascript API を使ってみます
Javascript から API をコールして Web サイトなどに地図を埋め込むことができます
チュートリアルにサンプルがあるので動かしてみます

  • vim maps.htm
<!DOCTYPE html>
<html>
  <head>
    <title>Simple Map</title>
    <meta name="viewport" content="initial-scale=1.0">
    <meta charset="utf-8">
    <style>
      #map {
        height: 100%;
      }
      html, body {
        height: 100%;
        margin: 0;
        padding: 0;
      }
    </style>
  </head>
  <body>
    <div id="map"></div>
    <script>
      var myLatLng = {lat: 35.6811716, lng: 139.7648629};
      var map;
      function initMap() {
        map = new google.maps.Map(document.getElementById('map'), {
          center: myLatLng,
          zoom: 15
        });
        var marker = new google.maps.Marker({
          position: myLatLng,
          map: map,
          title: 'Hello World!'
        });
      }
    </script>
    <script src="https://maps.googleapis.com/maps/api/js?key=xxxxxxxxxxxxx&callback=initMap"
    async defer></script>
  </body>
</html>
  • open maps.html

key=xxxxxxxxxxxxx の部分は先程取得した API Key を入力してください

これだけで Google Map アプリを Web 上に表示できます
特にピンなどは立てず単純に地図を表示しているだけです
やっていることは単純で <div id="map"></div> のところに Maps API をコールした結果を埋め込んでいるだけです

Maps API から結果が返ってくると initMap が呼ばれその内容に応じて初めに表示する地図の内容を変更します

center は地図が表示されたときの画面中央に来る座標です
座標情報は Google Map などを使って確認することが可能です
サンプルは東京駅の座標です
google_map_platform5.png

zoom はマップを表示した際の初期の拡大率を設定できます
値は大きくなればなるほど拡大されます
拡大率と値の関係は以下の通りです

  • 1: World -> 世界地図全部が見える
  • 5: Landmass/continent -> 大陸全体が見えるくらい拡大
  • 10: City -> 都市全体が見えるくらい拡大
  • 15: Streets -> 道が見えるくらい拡大
  • 20: Buildings -> 建物名が見えるくらい拡大

Directions API を使ってみる

今度は Routes の Directions API の機能を使ってみます
経路をマップ上に表示することができます

まずスタートとゴールをもとに経路情報を取得します

  • curl 'https://maps.googleapis.com/maps/api/directions/json?origin=Disneyland&destination=Universal+Studios+Hollywood&key=xxxxxxxxxxxxx'

すると JSON 情報がずらーっと取得できると思います
デフォルトだと車での経路情報になります
他にも mode があり driving, walking, bicycling, transit などが指定できるようです

出発点 (origin) と行き先 (destination) を自然言語で指定できるのは Directions API を使う上で嬉しいポイントかなと思います

応用: Maps Javascript API + Directions API で経路を Web 上に表示する

Directions API は単純に JSON を取得するだけです
もしマップ上に経路を表示したい場合は Maps Javascript API と連携します
他もそうですが可視化するには基本的には Maps のどれかと連携する必要があります
ここのサンプルを紹介します

  • vim maps_directions.html
<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="initial-scale=1.0, user-scalable=no">
    <meta charset="utf-8">
    <title>Directions Service</title>
    <style>
      #map {
        height: 100%;
      }
      html, body {
        height: 100%;
        margin: 0;
        padding: 0;
      }
      #floating-panel {
        position: absolute;
        top: 10px;
        left: 25%;
        z-index: 5;
        background-color: #fff;
        padding: 5px;
        border: 1px solid #999;
        text-align: center;
        font-family: 'Roboto','sans-serif';
        line-height: 30px;
        padding-left: 10px;
      }
    </style>
  </head>
  <body>
    <div id="floating-panel">
    <b>Start: </b>
    <select id="start">
      <option value="chicago, il">Chicago</option>
      <option value="st louis, mo">St Louis</option>
      <option value="joplin, mo">Joplin, MO</option>
      <option value="oklahoma city, ok">Oklahoma City</option>
      <option value="amarillo, tx">Amarillo</option>
      <option value="gallup, nm">Gallup, NM</option>
      <option value="flagstaff, az">Flagstaff, AZ</option>
      <option value="winona, az">Winona</option>
      <option value="kingman, az">Kingman</option>
      <option value="barstow, ca">Barstow</option>
      <option value="san bernardino, ca">San Bernardino</option>
      <option value="los angeles, ca">Los Angeles</option>
    </select>
    <b>End: </b>
    <select id="end">
      <option value="chicago, il">Chicago</option>
      <option value="st louis, mo">St Louis</option>
      <option value="joplin, mo">Joplin, MO</option>
      <option value="oklahoma city, ok">Oklahoma City</option>
      <option value="amarillo, tx">Amarillo</option>
      <option value="gallup, nm">Gallup, NM</option>
      <option value="flagstaff, az">Flagstaff, AZ</option>
      <option value="winona, az">Winona</option>
      <option value="kingman, az">Kingman</option>
      <option value="barstow, ca">Barstow</option>
      <option value="san bernardino, ca">San Bernardino</option>
      <option value="los angeles, ca">Los Angeles</option>
    </select>
    </div>
    <div id="map"></div>
    <script>
      function initMap() {
        var directionsService = new google.maps.DirectionsService;
        var directionsDisplay = new google.maps.DirectionsRenderer;
        var map = new google.maps.Map(document.getElementById('map'), {
          zoom: 7,
          center: {lat: 41.85, lng: -87.65}
        });
        directionsDisplay.setMap(map);

        var onChangeHandler = function() {
          calculateAndDisplayRoute(directionsService, directionsDisplay);
        };
        document.getElementById('start').addEventListener('change', onChangeHandler);
        document.getElementById('end').addEventListener('change', onChangeHandler);
      }

      function calculateAndDisplayRoute(directionsService, directionsDisplay) {
        directionsService.route({
          origin: document.getElementById('start').value,
          destination: document.getElementById('end').value,
          travelMode: 'DRIVING'
        }, function(response, status) {
          if (status === 'OK') {
            directionsDisplay.setDirections(response);
          } else {
            window.alert('Directions request failed due to ' + status);
          }
        });
      }
    </script>
    <script async defer
    src="https://maps.googleapis.com/maps/api/js?key=xxxxxxxxxxxxx&callback=initMap">
    </script>
  </body>
</html>
  • open maps_directions.html

少し長いですがそこまで難しいことはしていません
ポイントは google.maps.DirectionsService で JSON 情報を取得し google.maps.DirectionsRenderer でマップ上に経路情報を表示します
google.maps.DirectionsRenderer は google.maps.DirectionsService から取得したレスポンス情報をそのまま使えるようになっています (directionsDisplay.setDirections(response))

実際に経路情報を取得部分は directionsService.route になっており travelMode は 'DRIVING' になっています

あとは出発点と行き先を選択できるセレクトボックスの状態を監視して値が変化したら再度 DirectionsService を使って経路情報を取得するようにするだけです (document.getElementById('start').addEventListener('change', onChangeHandler))

google_map_platform4.png

テキスト情報を使って通知などで使う場合には Directions API だけでも十分かなと思います

Place Details API を使ってみる

最後に Place の Place Details API を使ってみます
この API は placeid と呼ばれる場所を特定する ID をもとにその場所の詳細情報を取得することができます

  • curl 'https://maps.googleapis.com/maps/api/place/details/json?placeid=ChIJN1t_tDeuEmsRUsoyG83frY4&fields=name,rating,formatted_phone_number&key=xxxxxxxxxxxxx'

サンプルは Google の本社の情報を取得しています

{
   "html_attributions" : [],
   "result" : {
      "formatted_phone_number" : "(02) 9374 4000",
      "name" : "Google",
      "rating" : 4.2
   },
   "status" : "OK"
}

Directions API 同様テキスト情報として取得できます
Maps Javascrip API と組み合わせてマップ上に表示したい場合はこの辺りを参考にしてみると良いかと思います

最後に

Google Map Platform の各プラットフォームの API を 1 つずつですが試してみました
経路や場所のテキスト情報を Routes や Places を使って取得して、それらを可視化したい場合に Maps を使うのが基本的な使い方だと思います

今回紹介した API はほんの一部です
Geolocation API や Street View API もあるので興味があれば調べてみてください
公式のドキュメントがかなり充実しているので特につまづくことなく使えると思います

おそろく強力な API なので使い方を知っておいて損はないかなと思います
作成された API Key は使用しないのであれば削除しておくことをおすすめします

参考サイト

2018年10月25日木曜日

git credential で独自のヘルパを作ってみた

概要

前回 git-credential のビルトインの機能を使ってみました
そこでヘルパは任意のスクリプトで実装できることを知りました
今回は独自のヘルパを Ruby + Redis で作ってみました
Redis に認証情報を格納してそれを取り出し Github などの認証ができるようになります

バージョン

  • Ubuntu 18.01
  • Ruby 2.5.1p57
  • Redis 4.0.9

事前準備

  • apt -y install redis-server

ライブラリインストール

  • gem install redis

スクリプトを PATH 上に配置する関係でグローバルインストールします

ヘルパスクリプト

  • vim /usr/local/bin/git-credential-redis-helper
#!/usr/bin/env ruby

require 'redis'
require 'json'
require 'erb'
require 'securerandom'

class RedisHelper
  def initialize
    @redis = Redis.new
  end

  def list
    @redis.keys.map { |key|
      {key => @redis.get(key)}
    }
  end

  def get
    known = {}
    while line = STDIN.gets
      break if line.strip == ''
      k,v = line.strip.split '=', 2
      known[k] = v
    end
    @auth = {}
    list.each { |auths|
      auths.each { |k,v|
        auth = JSON.parse(v)
        if auth["protocol"] == known["protocol"] and auth["host"] == known["host"]
          @auth = auth
          break
        end
      }
    }
    erb = ERB.new(File.read("/usr/local/bin/auth.erb"))
    erb.result(binding)
  end

  def store
  end  

  def qstore(key, value)
    @redis.set(key, value)
  end

  def erase(key)
    @redis.del(key)
  end
end

def main
  rh = RedisHelper.new
  command = ARGV[0]
  case command
  when 'list' then
    puts rh.list
  when 'get' then
    puts rh.get
  when 'store' then
    rh.store
  when 'qstore' then
    rh.qstore(ARGV[1], ARGV[2])
  when 'erase' then
    rh.erase(ARGV[1])
  else
  end
end
  • chmod 755 /usr/local/bin/git-credential-redis-helper

拡張子なしで作成するためシェバングを付与しています
git からコールされるため実行権限を付与します

説明

main 関数を用意したのでそこから見ると良いと思います
引数のコマンドに応じて redis に対する処理が変わってきます

get

必ず実装しなければいけないのが get です

get は標準入力を受け取って、その入力条件に当てはまる認証情報を redis から取得します
そして決められたフォーマットで認証情報を標準出力します
今回の redis のフォーマットは SET を使って key + hash として認証情報を登録することを想定しています
hash 側に認証情報があるのでそこと比較して当てはまるものがあれば erb でフォーマットして出力している感じです

redis に登録する際のフォーマットは改良の余地ありかなと思います
また、erb も必須ではありません
改行などもあるので、今回は使いましたが直接 puts しても問題ないです

qstore

引数に key + hash を取りそれをそのまま redis に格納します
store とコマンド名がかぶらないように q を付与しています

list

list は確認用の便利コマンドとして用意しました
redis 内にあるすべての key 情報に対してデータを取得して表示してくれます

erase

指定の key を redis から削除してくれます

store

不明
後で実行方法を紹介しますが、おそらくこれがちゃんと実装されていれば一度目の認証情報をちゃんと redis に格納できるようになるんだと思います

テンプレートファイルの配置

  • vim /usr/local/bin/auth.erb
protocol=<%= @auth["protocol"] %>
host=<%= @auth["host"] %>
username=<%= @auth["username"] %>
password=<%= @auth["password"] %>

これは正直なくてもいいかもしれません
直接スクリプト内で puts しても OK です

動作確認 (使い方)

redis-server は起動しておきましょう
また現状は localhost:6379 でのみ動作していることを想定しています

まず認証情報を登録します

git credential-redis-helper qstore 'github' '{"protocol":"https","host":"github.com","username":"hawksnowlog","password":"xxxxxxxx"}'

こんな感じです
別に Github の情報でなくても OK です
登録した認証情報は list コマンドで確認できます

  • git credential-redis-helper list

また git credential-redis-helper get はインタラクティブに認証情報を検索することができます
これは store や cache ヘルパと同じ仕様になります

登録したら認証が聞かれるか確認してみましょう
Github の場合であれば push 時に認証が聞かれなければ OK です

登録した認証情報を削除したい場合は

  • git credential-redis-helper erase github

で可能です
もちろん redis-cli でログインして DEL しても OK です

最後に

git credential で独自のヘルパスクリプトを実装してみました
今回は Ruby + Redis で開発しましたが、同じように別の言語や RDB でも実装可能です
store の挙動がよくわからなかったので今回の仕様では事前に redis に認証情報を登録する必要が出てしましました
本来は一度目の認証を redis に登録してそれ以降は redis にある認証情報を使いたかったのです

その辺りの問題を解決して、もう少しリファクタリングすれば公開できるかなと思います (ホスト名の指定やパスワード情報の暗号化なども実装したほうが良いかなと思います)
redis をインストールする必要もあるので需要があるか不明ですが