JavaScriptでArrayBuffer<->Base64URLの変換をおこなう

最近、JavaScriptを使ったArrayBufferとBase64URLの変換をちょくちょくおこなうようになりました。 調べてみるとやり方はちゃんと書いてあるものの、色々な実装方法があったり、サイトが散らばっていたりと毎回調べ直すのが面倒くさかったので、この記事でまとめておくことにしました。
(もっと良い実装はあるかもしれないけれど。。。)

JavaScriptを利用したNode.jsでの実装とブラウザでの実装をそれぞれまとめます。 また、Base64との変換、Stringとの変換もまとめました。

※2023/10/12編集:ブラウザでのArrayBufferとBase64URLまたはBase64との変換のコードを修正しました。Uint16Arrayを使うと文字数(バイト数)の都合で変換できないケースがあるため、Uint8Arrayを使うようにしました。

Node.js

Node.jsでのArrayBufferとBase64URLの変換はbase64urlモジュールをnpm installして利用します。 Base64からの変換もしくはBase64への変換は、同モジュールのfromBase64およびtoBase64を利用すればよいでしょう。

実装は以下のとおりです。

ついでにArrayBufferとStringの変換は以下の通りです。

ブラウザ

ブラウザでのArrayBufferとBase64URLまたはBase64との変換は以下の通りです。

ArrayBufferとStringの変換は以下のようになります。

おまけとして、ブラウザの方はCodePenで楽に実行環境がつくれたので貼っておきます。
変換結果がコンソールに出力されるようになっています。

Web Authentication API 7章 Relying Party処理の日本語訳

FIDO2に重要なWeb Authenticationの仕様の7章Relying Partyについて書かれている部分を邦訳した。
(表記ゆれは残っているし、誤訳もあるかも。。。)

この内容をもとにYahoo! JAPANでの生体認証の取り組み(FIDO2サーバーの仕組みについて) - Yahoo! JAPAN Tech Blogでは実装されているはずなので、参考にしてFIDOについて勉強していく予定。

FIDO2やWen Authenticationの基本的な内容は、以下の記事などを参照。


7. WebAuthn Relying Party処理

登録処理または認証処理は、WebAuthn Relying PartyがそれぞれPublicKeyCredentialCreationOptionsオブジェクトまたはPublicKeyCredentialRequestOptionsオブジェクトという処理に使用するパラメータをエンコードしたオブジェクトを作成することから始まります。Relying Partyはこの段階で機密情報を漏らさないように注意しなければなりません(SHOULD)。詳細は§ 14.6.2 Username Enumerationを参照してください。

create()またはget()の実行に成功した場合、Relying Partyスクリプトは、クライアントからそれぞれAuthenticatorAttestationResponse構造またはAuthenticatorAssertionResponse構造を含んだPublicKeyCredentialを受け取ります。本仕様外のメソッドによって、この構造のコンテンツはRelying Partyのサーバに届けられなければなりません。このセクションではRelying Partyが受け取ったこれらの構造のオブジェクトに対しておこなう処理について記述しています。

7.1 新しいクレデンシャルの登録

登録処理を実行するため、Relying Partyは以下のことを実施しなければなりません(MUST):

  1. options を、処理のためにRelying Partyが必要とする新規のPublicKeyCredentialCreationOptions構造に設定する。
  2. navigator.credentials.create()を呼び出し、 publicKeyオプションとして option を受け渡す。Promiseが正常にresolveされた結果を credintial とする。Promiseがrejectされた場合、ユーザが見ることができるエラーによって処理を中止する、もしくはrejectされたPromiseから取得できる情報から判定できるユーザ操作に導く。例えば、「InvalidStateError」と同等なエラーコードによってPromiseがrejectされた場合、別の認証器を利用するようにユーザに指示する。その他エラーおよび環境での情報は、§ 6.3.2 The authenticatorMakeCredential Operationを参照。
  3. responsecredential.response とする。 responseAuthenticatorAttestationResponseインスタンスでなかった場合、ユーザが見ることができエラーによって処理を中止する。
  4. clientExtensionResultscredential.getClientExtensionResults() の呼び出し結果を設定します。
  5. JSONtextresponse.clientDataJSON の値にUTF-8デコードを実行した結果を設定します。

注意:UTF-8デコードの他の実装を利用することは、UTF-8デコードアルゴリズムと同じ結果を出力するかぎり認められています。特に先頭のバイト順マーク(BOM)は取り除かなければなりません(MUST)。

  1. C 、クレデンシャル作成時に収集されたクライアントデータJSONtext に対して実装特有のJSONパーサを実行した結果を設定します。

注意: C はこのアルゴリズムが必要なときに Cコンポーネントを参照可能なかぎり、実装特有なデータ構造表現にして構わない。

  1. C.type の値がwebauthn.createと一致することを検証する。
  2. C.challenge の値が options.challenge のBase64URLエンコーディング結果と一致することを検証する。
  3. C.origin の結果がRelying Partyoriginと一致することを検証する。
  4. C.tokenBinding.status の値が認証を取得するTLS接続のためのトークンバインディングの状態と一致することを検証する。また、TLS接続においてトークンバインディングが使われた場合、 C.tokenBinding.id がその接続のトークンバインディングIDBase64URLエンコーディングに一致することを検証する。
  5. hash にSHA-256を利用して response.clientDataJSONハッシュ値を計算した結果を設定する。
  6. attestation statement format fmtauthenticator data authData およびattestation statement attStmt を取得するため、 AuthenticatorAttestationResponse構造のattestationObjectフィールドに対してCBORデコーディングを実行する。
  7. authData 内のrpIdHashRelying Partyによって期待されるRP IDのSHA-256ハッシュ値であることを検証する。
  8. authData 内のflagsUser Presentビットが設定されていることを検証する。
  9. この登録にユーザ認証を必須とする場合、 authData 内のflagsUser Verifiedビットが設定されていることを検証する。
  10. authData 内の公開鍵の「alg」パラメータが options.pubKeyCredParams 内の要素のうちの1つのalg属性と一致することを検証する。
  11. clientExtensionResults 内のclient extension outputsおよび authData 内のextensionsauthenticator extension outputsの値が期待通りであることを検証する。これは options.extensions で渡されたclient extension inputの値および未承認、つまり options.extensions 部分に記載されていないextensionsに関するRelying Party独自のポリシーについて考慮したものである。一般的に、「期待通りであること」という意味はRelying Partyとどのようなextensionsが利用されているかによって変わる。

注意:クライアントプラットフォームは、追加的なauthenticator extensionsまたはclient extensionsを設定するためのローカルポリシーを規定するかもしれない(MAY)。つまり、これらは options.extensions 部分で元来定められていなかったauthenticator extension outputsまたはclient extension outputsを表現する値を必要とする。Relying Partyは未承認のextensionsを無視するかattestationを拒絶するかに関わらず、このような状態に対応できるようにしなければならない(MUST)。Relying Partyはローカルポリシーと利用されているextensionsに基づいてこの決定をおこなうことができる。

注意:クライアント認証器の両方において全てのextensionsはOPTIONALであるため、Relying Partyは必須となるextensionsが全て実行されないケースおよび一部が実行されないケースに対応できるようにしなければならない(MUST)。

  1. サポートされているWebAuthn Attestation Statement Format Identifierに対し、 fmt がUSASCIIケースセンシティブに一致するかを確認してattestation statement formatを決定する。登録されているWebAuthn Attestation Statement Format Identifierの最新のリストは、[RFC8809]によって設立されたIANA "WebAuthn Attestation Statement Format Identifiers" registry [IANA-WebAuthn-Registries]にて管理されている。
  2. attStmt が適切なattestation signatureを有した正しいattestation statementであることを、与えられた attStmtauthData および hash を用いてattestation statement format fmt ごとの検証手順によって検証する。

注意:それぞれの attestation statement formatは独自の検証手順を有する。初期に定義されたフォーマットについては§ 8 Defined Attestation Statement Formatsを参照し、最新のリストは[IANA-WebAuthn-Registries]を参照する。

  1. 検証に成功した場合、信頼されたソースおよびポリシーからattestation typeおよびattestation statement format fmt に関するアクセス可能なトラストアンカー(つまり、attestation root certificates)の一覧を取得できる。例えば、FIDO Metadata Service [FIDOMetadataService]は、 authData 内のattestedCredentialDataaaguidを利用してそれらの情報を取得する方法の1つである。
  2. ステップ19の検証手順の結果を利用し、以下のようにattestationの信用性を評価する:
  3. credentialIdがどのユーザに対して登録されていないことを確認する。既に他のユーザに登録されているクレデンシャルの登録が要求された場合、Relying Partyはこの登録処理を失敗させるべきである(SHOULD)、または例えば古い登録を削除して登録を受け入れると決めることができる(MAY)。
  4. attestation statement attStmt の検証が成功し、信用性が確認され、 options.user で示されているアカウントに対して新しいクレデンシャルとして登録された場合:
  5. credentialIdcredential.response.getTransports() を呼び出した戻り値であるtransport hintsを関連付ける。この値は保存前後で変更すべきではない(SHOULD NOT)。後述のget()の呼び出しにおいてClientが適切な認証器を見つける方法を知る手助けとなるようにallowCredentialsオプションのtransportsに情報を入れるため、この値を使うことを推奨する(RECOMMENDED)。
  6. attestation statement attStmt の検証に成功したが上述のステップ21で信用性が確認できなかった場合、Relying Party登録処理を失敗させるべきである(SHOULD)。

注意:ただし、ポリシーによって許可された場合、Relying Partycredential IDおよび公開鍵を登録することができる(MAY)が、self attestation§ 6.5.3 Attestation Types参照)と関連付いていることとなり、クレデンシャルを脅かすことになる。これを実施した場合、Relying Partyは特定の認証器モデルによって生成された公開鍵は暗号証明がないことを検証する。更に詳細な議論は[FIDOSecRef]および[UAFProtocol]を参照。

attestation objectsの検証は、Relying Partyが上述のステップ20においてアクセス可能なトラストアンカーを決定するための信用できる方法を有していることが必須である。また、証明書が使用された場合、Relying Partyは中間CA証明書に証明書ステータス情報にアクセスしなくてはならない(MUST)。さらに、Clientがattestation情報でattestation certificate chainについて知らせなかった場合、Relying Partyはそのチェーンを構成することができなければならない。

7.2 Authenticaiton Assertionの検証

認証処理を実行するため、Relying Partyは以下の内容を実行する必要がある(MUST):

  1. options に、Relying Partyが処理に必要とする項目を設定した新規生成したPublicKeyCredentialRequestOptions構造を設定する。 options.allowCredentials が存在する場合、それぞれのアイテムtransports要素は対応するクレデンシャルが登録されたときの credential.response.getTransports() の戻り値を設定するべきである(SHOULD)。
  2. navigator.credentials.get()を呼び出し、publicKeyオプションとして options を受け渡す。 credential にPromiseのresolveが成功した結果を設定する。Promiseがrejectされた場合、ユーザが見ることができるエラーによって処理を中止する、もしくはrejectされたPromiseから取得できる情報から判定できるユーザ操作に導く。その他エラーおよび環境での情報は、§ 6.3.3 The authenticatorGetAssertion Operationを参照。
  3. responsecredential.response を設定する。 responseAuthenticatorAssertionResponseインスタンスではない場合、ユーザが見ることができるエラーによって処理を中止する。
  4. clientExtensionResultscredential.getClientExtensionResults() の呼び出し結果を設定する。
  5. options.allowCredentials空ではない場合、 credential.idoptions.allowCredentials に挙げられている公開鍵の1つを指定していることを検証する。
  6. 認証されたユーザであることを特定し、そのユーザが credential.id で指定された公開鍵認証ソース credentialSource のオーナーであることを検証する:
    認証処理を開始する前に例えばユーザ名やcookieによりユーザが特定されていた場合、
    特定されたユーザが credentialSource のオーナーであることを検証する。 response.userHandle が存在する場合、 userHandle にその値を設定する。また、 userHandle が同じユーザに紐付いていることを検証する。
    認証処理を開始する前にユーザが特定されていなかった場合、
    response.userHandle が存在することおよびその値によって特定されたユーザが credentialSource のオーナーであることを検証する。
  7. credential.id (または、base64urlエンコーディングが自身のユースケースに不適切な場合にはcredential.rawId)を利用して対応する公開鍵を探し出し、 credentialPublicKey公開鍵を設定する。
  8. cDataauthData および sig に対して、それぞれ responseclientDataJSONauthenticatorDataおよびsignatureの値を割り当てる。
  9. JSONtextresponse.clientDataJSON の値にUTF-8デコードを実行した結果を設定します。

注意:UTF-8デコードの他の実装を利用することは、UTF-8デコードアルゴリズムと同じ結果を出力するかぎり認められています。特に先頭のバイト順マーク(BOM)は取り除かなければなりません(MUST)。

  1. C 、クレデンシャル作成時に収集されたクライアントデータJSONtext に対して実装特有のJSONパーサを実行した結果を設定します。

注意: C はこのアルゴリズムが必要なときに Cコンポーネントを参照可能なかぎり、実装特有なデータ構造表現にして構わない。

  1. c.type の値がwebauthn.getという文字列であることを検証する。
  2. C.challenge の値が options.challenge のBase64URLエンコーディング結果と一致することを検証する。
  3. C.origin の結果がRelying Partyoriginと一致することを検証する。
  4. C.tokenBinding.status の値がattestationを取得したTLS接続のためのトークンバインディングの状態と一致することを検証する。また、TLS接続においてトークンバインディングが使われた場合、 C.tokenBinding.id がその接続のトークンバインディングIDBase64URLエンコーディングに一致することを検証する。
  5. authData 内のrpIdHashRelying Partyによって期待されるRP IDのSHA-256ハッシュ値であることを検証する。

注意:appid extensionを利用する場合、このステップで特別なロジックが必要となります。詳細は§ 10.1 FIDO AppID Extension (appid)を参照。

  1. authData 内のflagsUser Presentビットが設定されていることを検証する。
  2. この認証にユーザ認証を必須とする場合、 authData 内のflagsUser Verifiedビットが設定されていることを検証する。
  3. clientExtensionResults 内のclient extension outputsおよび authData 内のextensionsauthenticator extension outputsの値が期待通りであることを検証する。これは options.extensions で渡されたclient extension inputの値および未承認、つまり options.extensions 部分に記載されていないextensionsに関するRelying Party独自のポリシーについて考慮したものである。一般的に、「期待通りであること」という意味はRelying Partyとどのようなextensionsが利用されているかによって変わる。

注意:クライアントプラットフォームは、追加的なauthenticator extensionsまたはclient extensionsを設定するためのローカルポリシーを規定するかもしれない(MAY)。つまり、これらは options.extensions 部分で元来定められていなかったauthenticator extension outputsまたはclient extension outputsを表現する値を必要とする。Relying Partyは未承認のextensionsを無視するかattestationを拒絶するかに関わらず、このような状態に対応できるようにしなければならない(MUST)。Relying Partyはローカルポリシーと利用されているextensionsに基づいてこの決定をおこなうことができる。

注意:クライアント認証器の両方において全てのextensionsはOPTIONALであるため、Relying Partyは必須となるextensionsが全て実行されないケースおよび一部が実行されないケースに対応できるようにしなければならない(MUST)。

  1. hash にSHA-256を利用して cDataハッシュ値を計算した結果を設定する。
  2. credentialPublicKey を使用し、 sigauthDatahash のバイナリ連結に対する適切な署名であることを検証する。

注意:この検証ステップはFIDO U2F認証器によって生成された署名に互換性がある。§ 6.1.2 FIDO U2F Signature Format Compatibility参照。

  1. storedSignCountcredential.id の関連付けられて保存されたsignature counterの値を設定する。authData.signCount がゼロではない場合、もしくは storedSignCount がゼロではない場合、以下の追加ステップを実行する:
    • authData.signCount が、
      storedSignCount より大きい場合、
      authData.signCount の値に storedSignCount を更新する。
      storedSignCount より小さいもしくは同じ場合、
      認証器が複製されている可能性がある、つまり秘密鍵の複製が最低でも2つ存在している可能性があり、同時に利用されていることを示唆する。Relying Partyはこの情報をリスクスコアリングに組み入れるべきである。このような場合、Relying PartystoredSignCont を更新するか更新しないか、もしくは認証処理を失敗させるか失敗させないかについてはRelying Partyに依存する。
  2. 上述の全てのステップが成功した場合、必要に応じて認証処理を続けます。そうでなければ、認証処理を失敗させる。

Jekyll TeXt Themeのアーカイブページにポスト以外のページとタグを表示する

JekyllとGitHub pagesを使って、個人的な技術メモサイトをつくりました(がんばって記事を書こう...)。

ページのテーマはTeXt themeを使っています。
シンプルな見た目のウェブサイトで、サイドバーや目次をページに追加できるなど見やすいメモサイトができると考えて使っています。

ちょっと改善したい所

Text themeにはアーカイブページというページが用意されています。デモサイトではこのようなページとなっています。

このアーカイブページは、ポスト記事(日付で整理される記事、_postフォルダ下のファイル)の一覧とポスト記事についているタグを集めて表示してくれます。また、タグが付いているページ数をカウントし、それも合わせて表示します。さらにタグを選択すると、そのタグが付いているページだけが表示されます。

しかしながら、アーカイブページは ポスト以外のページ の表示やタグの収集はおこないません。今回の私のサイトは日付で管理するブログ的なサイトというより、勉強したことを追記するメモサイトなので、ポスト以外のページも対象にしたいところです。

対応したこと・できていないこと

上記の困りごとのため、ソースコードを改変し、ざっくりと以下の内容に対応しました。

  • Text themeのアーカイブページにポスト以外のページを一覧表示させる
  • Text themeのアーカイブページにポスト以外のページに付いているタグを収集させる
      - 収集したタグを選択するとそのタグが付いているページのみ一覧表示される
    

できていないことは以下です。

  • タグがついているページ数をカウントする

改変後のアーカイブページはこちらです。

改変したこと

先に設定の変更とソースコードの改変部分を挙げておきます。

  • Jekyllのコレクションを使ってページを管理するように_config.xmlを変更
    • コレクションをまとめるディレクトリ(collection_dir)を利用する
  • archive.htmlでポスト以外のページも一覧にする
  • tags.htmlでポスト以外のページのタグを収集し、表示させる

_config.xmlの変更

Jekyllのコレクション(collections)とは、ページをまとめた配列を作成してくれるJekyll標準機能です。

<source>/_hogeという形式でフォルダを作成し、そのフォルダ以下にページを作成すると、site.collections.hogeという配列になります。

<source>はデフォルトではトップページとなりますが、_config.xmlcollection_dirというプロパティを使用することで別のフォルダにすることができます。collection_dirを使った場合には、ポスト記事のフォルダ(_post)や下書きのフォルダ(_draft)もcollection_dirで指定したフォルダ以下に配置する必要があります。

今回は_config.xmlに以下の設定のような設定を追加します。
articlesフォルダ以下にコレクションとなるフォルダを用意します。下のような場合、_java_nodejsというフォルダを用意することになります。

collections_dir: articles
collections:
  java:
    output: true
  nodejs:
    output: true

archive.htmlの改変

archive.htmlは、元々は全てのポスト記事を一覧にして表示してくれるページです。

ここでは、先述のように設定したコレクションを用い、ポスト記事以外のページもこのアーカイブページに表示させます。

具体的には、archive.htmlを以下のように変更します。

変更前

    {% assign my_list = site.posts | concat: site.nodejs | concat: site.oidc_oauth %}
    {%- include article-list.html articles=my_list type='brief' show_info=true reverse=true group_by='year' -%}

変更後

    {%- assign page_list = "" | split: ',' -%}
    {%- for coll in site.collections -%}
      {%- assign page_list = page_list | concat: coll.docs -%}
    {%- endfor -%}
    {%- include article-list.html articles=page_list type='brief' show_info=true reverse=true group_by='year' -%}

tags.htmlの改変

tags.htmlは元々アーカイブページ(archive.html)に読み込まれ、全てのポスト記事から収集したタグの一覧を表示します。タグをクリックすると、そのタグが付いているページのみ表示されるようになります。

今回は修正をおこない、ポスト記事以外の全てのページからタグを収集するように変更します。 最終的に_tagsという変数にタグ情報を格納することで、アーカイブページにタグ機能を利用できるようになります。もともと、変数_tagsにはJekyllに用意されているsite.tagsを格納しています。このsite.tagsはポスト記事についているタグだけが入っています。そこで、全てのページのタグを集め、site.tagsの代わりに同じ構造のオブジェクトを作成してそれを渡せばよいはずです。

site.tagsの構造を説明します。site.tagsは配列になっています。配列の各要素は、さらに配列となっています。この要素の配列の先頭はタグの名称です。配列の2番目はそのタグがつけられているページ情報の配列です。

全てのページのタグから作成したタグ情報のオブジェクトの配列(site.tagsの代替、以下では「タグ情報オブジェクト配列」と呼称)を作成するため、コレクションを使います。 全てのコレクションを取り出し、各コレクションからページを取り出し、ページに付いているタグを取り出します。これらはfor文によるループで実行します。取得したタグは、作成するタグ情報オブジェクト配列に同じ名称のタグがなければ新規に追加し、反対に既に存在すればページ情報の配列に追加します。

変更内容

tags.htmlの先頭に追加します。

{%- assign page_tags = "" | split: ',' -%}

{%- for coll in site.collections -%}
  {%- for page in coll.docs -%}
    {%- for tagname in page.tags -%}
      {%- assign found = false -%}
      {%- for target in page_tags -%}
        {%- if target[0] == tagname -%}
          {%- comment -%}
          下の行がうまく実行できていない
          {%- endcomment -%}
          {%- assign target[1] = target[1] | push: page -%}
          {%- assign found = true -%}
          {%- break -%}
        {%- endif -%}
      {%- endfor -%}
      {%- if found == false -%}
        {%- assign tagstruct = "" | split: ',' -%}
        {%- assign tagstruct = tagstruct | push: tagname -%}
        {%- assign page_list = "" | split: ',' -%}
        {%- assign page_list = page_list | push: page -%}
        {%- assign tagstruct = tagstruct | push: page_list -%}
        {%- assign page_tags = page_tags | push: tagstruct -%}
      {%- endif -%}
    {%- endfor -%}
  {%- endfor -%}
{%- endfor -%}

下のコードを、

{%- assign _tags = site.tags | sort -%}

以下のように変更します。

{%- assign _tags = page_tags | sort -%}

残っている課題

記事の先頭にも記載していますが、上のコードではできていないことがあります。

tags.htmlは元々タグが付いているページ数を表示するようになっていますが、今回の改修をおこなうとページ数が正しく表示できません。tags.htmlの実装コードにもコメントとして書いていますが、対象のタグが付いているページの配列に要素を追加することができていないです。 これは私がLiquid言語の配列の仕様を理解していないため、正しい実装をできていないことが原因のようです。

また、このページの配列が正しくセットされていなくとも、タグをクリックすることでページが絞り込まれる機能は動作しました。 アーカイブページはこちらです。

参考

JenkinsのPipeline Best Practicesを邦訳したので後で読み直す

最近Jenkinsを使うようになり、Jenkinsfileの書き方などをちょっと勉強するようになりました。

Pipeline Best Practicesという有用そうな記事を後で読み直したいので日本語訳して残します。もう少し勉強して記事の内容がわかるようになったら、もう一度読み直すつもりです。


Pipeline Best Practices

このガイドはいくつかのパイプラインのベストプラクティスを示し、最もありがちな間違いを指摘します。

ゴールは、パイプラインの作成者およびメンテナーがより良いパイプラインの実行となるパターンを目指し、知らず識らずに嵌ってしまう落とし穴を避けるようにすることです。このガイドは考えられるすべてのパイプラインのベストプラクティスを網羅したリストではなく、一般的な手法を見出すために有用ないくつかの具体的な事例を紹介します。一般的に「これをおこなう」ということを知るために利用し、驚くほど詳細な「ハウツー」としては利用しないでください。

一般

パイプラインの接着剤としてGroovyコードを使用する

パイプラインの主たる機能としてではなく、一連のアクションをつなげるためにGroovyコードを利用してください。言い換えると、ビルドプロセスを進めるためにパイプラインの機能性(Groovyやパイプラインステップ)に依存するのではなく、ビルドの複数のパートを完了させるためにシングルステップ(shのような)を使ってください。複雑性の増したパイプライン(Groovyコードの量、使用されるステップ数など)は、コントローラにより多くのコンピューターリソース(CPU、メモリ、ストレージ)を必要とします。ビルドのコアではなく、ビルドを完了させるためのツールとしてパイプラインを捉えてください。

例:単一のMavenビルドステップを使い、build/test/deployプロセスのビルドを進める

パイプラインで複雑なGroovyコードを避ける

パイプラインのため、Groovyコードは常にコントローラで実行され、コントローラのリソース(メモリおよびCPU)を使用します。それゆえ、パイプラインで実行されるGroovyコードの量(パイプラインにインポートされるクラスで呼び出されるメソッドを含む)を減らすことは非常に重要です。以下は、利用を避けるべき最も一般的なGroovyメソッドの例になります:

  1. JsonSlurper:この機能(およびXmlSlurperやreadFileのような類似した機能)はディスク上のファイルを読み取り、ファイルのデータをJSONオブジェクトに変換し、JsonSlurper().parseText(readFile("$LOCAL_FILE"))のようなコマンドを利用してパイプラインにオブジェクトを注入します。このコマンドはコントローラのメモリにローカルファイルを2回ロードします。ファイルが非常に大きい場合やコマンドが頻繁に実行される場合、大量のメモリが必要となります。
    1. ソリューション:JsonSlurperを使用する代わりに、シェルステップを使用して標準出力を返してください。このシェルは以下のようになります:def JsonReturn = sh label: '', returnStdout: true, script: 'echo "$LOCAL_FILE"| jq "$PARSING_QUERY"'。これはファイルの読み取りのためにエージェントのリソースを使い、$PARSING_QUERYはファイルをより小さいサイズに変換することに役立ちます。
  2. HttpRequest:このコマンドは外部ソースからデータを取得し、変数に格納するためによく利用されます。このプラクティスは理想的ではありません。コントローラから直接リクエストが発行される(コントローラに証明書が存在しない場合、HTTPリクエストに対して正しくない結果が返ってくる)だけでなく、リクエストに対するレスポンスが2回格納されます。
    1. ソリューション:エージェントからHTTPリクエストを実行させるためにシェルステップ、必要に応じてcurlwgetのようなツールを使用します。結果がパイプラインの後半で必要な場合、エージェント側で可能な限り結果をフィルダリングし、最小限の必須情報をJenkinsコントローラに返すべきです

類似したパイプラインステップの繰り返しを減らす

パイプラインの複数のステップを単一のステップに結合することは、パイプラインの実行エンジン自体のオーバーヘッドを減らすためにしばしば利用されます。例えば、連続して3つのシェルステップを実行する場合、ステップごとに開始と停止をしなければならず、作成と片付けのためにはエージェントとコントローラのコネクションとリソースが必要となります。しかし、すべてのコマンドを単一のシェルステップに入れる場合、たった1つのステップを開始および停止することが必要です。

例:echoshステップの作成を避け、それらを1つのステップまたはスクリプトに結合する。

Jenkins.getInstanceの呼び出しを避ける

パイプラインや共有ライブラリでJenkins.instanceやそのアクセッサメソッドを利用することは、パイプラインや共有ライブラリ内でのコードの誤用を表します。サンドボックス化されていない共有ライブラリからのJenkins APIの呼び出しは、その共有ライブラリが共有ライブラリであると同時にある種のJenkinsプラグインであることを意味します。パイプラインでのJenkins APIの呼び出しは重大なセキュリティ問題およびパフォーマンス問題を避けるため、非常に注意深くなる必要があります。ビルド時にJenkins APIを利用しなければならない場合、推奨アプローチはパイプラインのステップAPIを利用し、アクセスしたいJenkins APIを安全なラッパーでくるむように実装したJavaの小さいプラグインを作成することです。サンドボックス化されたJenkinsfileからJenkins APIを直接使用するということは、パイプラインを編集できる人によってサンドボックス保護をバイパスできるようなメソッドをホワイトリストに登録する必要があることを意味します。これは重大なセキュリティリスクです。ホワイトリストに登録されたメソッドは管理者権限を持つシステムユーザによって実行され、開発者が意図しているよりも大きな権限で実行されることになります。

ソリューション:ベストソリューションは呼び出しをおこなわないことですが、実行する必要がある場合には必要なデータを収集できるJenkinsプラグインを実装することを推奨します。

共有ライブラリを利用する

組み込みパイプラインステップをオーバーライドしない

可能な限り、カスタマイズ/上書きされたパイプラインステップを避けるべきです。組み込みパイプラインステップのオーバーライドは、shtimeoutのような標準パイプラインAPIを上書きするような共有ライブラリを利用する処理を指します。パイプラインAPIはいつでも変更できるため、この処理は危険です。カスタムコードが壊れたり、期待と異なる結果をもたらしたりします。パイプラインAPIの変更によってカスタムコードが破壊されたとき、カスタムコードが変更されてなくともAPIの変更後に同じように動作するわけではないため、トラブルシューティングが難しくなります。カスタムコードが変更されなかった場合でさえ、API更新後に同じように動作するわけではありません。最後に、パイプライン全体でこれらのステップが広く使われているため、何かが正しく実装されていない/非効率に実装されている場合にJenkinsに壊滅的な結果をもたらします。

巨大なグローバル変数宣言ファイルを避ける

巨大な変数宣言ファイルを持つことは、メリットがわずかもしくは全くない代わりに変数の必要有無に関わらずすべてのパイプラインにファイルがロードされるため膨大なメモリを必要とします。現在のジョブに関連する変数だけを含んだ小さな変数ファイルを作成することを推奨します。

非常に巨大な共有ライブラリを避ける

パイプラインで巨大な共有ライブラリを利用することは、パイプラインを開始する前に非常に巨大なファイルをチェックアウトし、実行中のジョブごとに同じ共有ライブラリをロードすることを必要とします。これはメモリオーバーヘッドを増やし、実行時間を遅くすることになります。

追加のFAQへの回答

パイプラインでの並列処理

複数のパイプライン実行または複数の異なるパイプライン同士でワークスペースを共有しないようにしてください。このプラクティスは、各パイプラインまたはワークスペースでの改名を含む予期しないファイル変更を引き起こす可能性があります。

理想的には、共有ボリュームやディスクは別の場所にマウントされ、その場所から現在のワークスペースにファイルがコピーされます。そして、ビルドが完了すると、更新があった場合にファイルをコピーして戻すことができます。

必要なリソースをスクラッチで作成する個別のコンテナでビルドすべきです(クラウドタイプのエージェントはこれに最適です)。これらのコンテナをビルドすることは、いつでもビルド処理を開始し、簡単に繰り返せることを保証します。コンテナのビルドが機能しない場合、パイプラインで同時実行を無効にするか、Lockable Resourcesプラグインを使用し、実行時にワークスペースをロックし、ロック中は他のビルドがワークスペースを利用できないようにしてください。注意:同時実行を無効にしたり、実行中にワークスペースをロックしたりすることは任意のリソースがロックされている場合、リソースの待機中にパイプラインがロックされます。

また、これらの両方の手法はジョブごとに一意のリソースを利用するよりもビルド結果が出るまでに時間がかかることになります。

NotSerializableExceptionを避ける

パイプラインコードはCPS変換されるため、パイプラインはJenkins再起動後に再開することができます。パイプラインがスクリプトを実行している間、Jenkinsをシャットダウンしたり、エージェントとの接続を切断したりすることができます。再開したとき、Jenkinsは何を実行していたのかを覚えており、パイプラインのスクリプトは中断されなかったかのように処理を再開できます。「continuation-passing style(CPS)」実行として知られるテクニックはパイプラインの再開で重要な役割を果たします。しかし、いくつかのGroovy表現はCPS変換の結果として正しく動作しません。

内部的には、CPSはパイプラインの現在の状態とともに実行予定のパイプラインの残りがシリアライズ化可能であることに依存します。これはシリアライズ化できないオブジェクトをパイプラインで利用することは、パイプラインが状態を永続化しようとするとき、NotSerializableExceptionをスローさせることに意味します。

詳細な情報と問題となりうるいくつかの例は、Pipeline CPS method mismatchesを御覧ください。

以下では期待するようにパイプラインが機能することを保証するテクニックを紹介します。

永続変数がシリアライズ可能であることを保証する

ローカル変数は、シリアライズ化中にパイプラインの状態の一部としてキャプチャされます。これは、パイプライン実行に変数内のシリアライズ化できないオブジェクトを保存する際にNotSerializableExceptionがスローされることを意味します。

変数にシリアライズ不可オブジェクトを割り当てない

シリアライズ化できないオブジェクトを使う手法の1つは、値を計算して変数に格納する代わりに常に「ジャストインタイム」で値を推測させることです。

@NonCPSを利用する

必要であれば、CPS変換されると正しく実行できなくなる特定のメソッドのために@NonCPSアノテーションを利用してCPS変換を無効化できます。これは、Groovy関数が変換されていないため、完全に再実行させる必要があることに注意してください。

非同期なパイプラインステップ(shsleepのような)は常にCSP変換され、@NonCPSアノテーションがついたメソッドの内部で利用することができません。一般的に@NonCPSアノテーション付きメソッドの内部でパイプラインステップを利用することを避けるべきです。

パイプラインの耐久性

パイプラインの耐久性を変更することは、スローされるはずの箇所でNotSerializableExceptionがスローされないことは注目に値します。PERFORMANCE_OPTIMIZEDによってパイプラインの耐久性を低下させることはパイプラインの現在の状態を永続化させる頻度を大幅に低下させることを意味するからです。つまり、パイプラインはシリアライズ化できない値をシリアライズ化しようとしなくなり、結果として例外がスローされなくなります。

このメモは、この操作の根本的な原因についてユーザに。パイプラインの耐久性の設定はパフォーマンス最適化のためだけに操作し、シリアライズ化問題を避けるためにおこなうことは推奨されません。

さよならBrackets

先日Bracketsがサポート終了となり、VS Codeへの移行が推奨と発表されました。 少し前まで愛用していたエディタだったので、ここでは追悼的な記事を書き残します。Adobeのサポートが終わるだけなので、もしかすると今後さらに発展するかもしれないですし。。。

2021 年 9 月 1 日、AdobeBrackets のサポートを終了します。 Brackets の使用、保守、改善を継続する場合は、GitHub でプロジェクトをフォークできます。 Visual Studio Code は、オープンソース上に構築された Microsoft の無料コードエディターです。AdobeMicrosoft とパートナー関係にあり、このエディターへの移行をお勧めします。

写真はHOTEL EDITというホテルのマグカップです。
Bracketsっぽくておしゃれ。

ホテル エディットのマグ

Bracketsとは

Brackets - Web デザインを認識する最新のオープンソースコードエディター

Bracketsとはオープンソーステキストエディタです。ちょっとリッチなエディタで、IDEのように重くないといったエディタです。

標準でライブプレビュー機能を持っているということが売りのひとつだと思います。ライブプレビューとは、Bracketsと連携させたGoogle ChromeブラウザでBracketsで編集したHTML・CSSファイルの内容が即座に反映されるという機能です。
HTML・CSS・JSの勉強をするときに非常に便利でした。というか、もともとEclipseしか使ったことがなかったとき、Webフロント周りの勉強をするために便利なエディタを探していて見つけたのがBracketsでした。

初めて使ったときには以下のQiitaの記事を参考にセットアップしました。

Bracketsの機能紹介、使い方解説 - Qiita

記事の内容はさらにまとめられて、書籍になっています。
Webのための次世代エディタ Bracketsの教科書 | 半田 惇志 | 工学 | Kindleストア | Amazon

プラグイン(エクステンション)

Bracketsではプラグインをインストールし、組み合わせることでエディタをカスタマイズするようになっていました。

ユーザからも様々なプラグインが提供されており、例えば、エディタの表示を変えたり、ファイルツリーのアイコンを変えたり、特にマークダウンのライブプレビューにはお世話になりました。

使っていたプラグインについて書き残します。とは言っても、Brackets おすすめエクステンション集・解説 - Qiitaで紹介されているプラグインがほとんどですが。。。

記事で紹介されているエクステンション

asgerf/bracket-rename: Rename refactoring for bracket

記事中では「Refactoring Tools for JavaScript」と紹介されていました。変数名や関数名を一括で変更できるエクステンションです。リファクタリングのときに便利です。

brackets-beautify/brackets-beautify: Beautify HTML, CSS, and Javascript in Adobe Brackets

ソースコードの整形をおこなうBeautifyのエクステンションです。

cmgddd/Brackets-css-color-preview: css color preview extension for Brackets

CSSファイルで色を指定している行の左端に、実際の色を表示してくれます。

assialiholic/brackets-highlight-multibyte-symbols: Highlight multibyte symbols and alphabets.(including Show Whitespace by DennisKehrig)

マルチバイト文字(全角文字)の英数字および記号をハイライトしてくれます。なぜか動かないJSにありがちな打ち間違いを探すときに便利です。

lkcampbell/brackets-indent-guides: A Brackets extension to show indent guides in the code editor

インデントを見やすく表示してくれます。なぜか動かないJSにありがちなカッコの過不足を探すときに便利です。

brackets-key-remapper

ショートカットキーのカスタマイズをBracketsデフォルトを含めて変更できるようにするエクステンション。エクステンションを追加していくと重複したり、そもそもBrackets以外のショートカットキーと重複したりするので、その解消に使うそうです。個人的にはあまり利用しませんでした。

Bitbucketで公開されていたリポジトリが削除されたかなにかでソースコードが見られなくなっている。

gruehle/MarkdownPreview: Brackets extension for previewing markdown files

Markdownファイルのプレビューをできるようにするエクステンション。とても利用しました。

abagshaw/brackets-minifier: Minifies and Concatenates JS and CSS in Brackets using UglifyJS3 and CleanCSS

JS、CSSのminifyするためのエクステンション。正直あまり使わなかった。

konstantinkobs/brackets-colorHints: Get all used hex colors in the current CSS file as code hints

CSSファイル内で同じ色を指定するときにサジェストをしてくれます。

alessandrio/custom-work-for-brackets

複数ファイルをタブで開けるようにしたり、分割表示できるようにしたりするエクステンションです。

この手のエクステンションを複数入れると、表示がおかしくなったりBracketsが起動しなくなったりするので注意です。

dnbard/brackets-extension-rating: Brackets extension used to display other extensions rating

Bracketsに標準搭載されているエクステンション検索のソート機能を強化するエクステンションです。

yasinkuyu/brackets-tools: Brackets developer tools extension

プログラミングでよくおこなう大文字・小文字の変換など諸々を解消してくれる便利エクステンションです。

機能が多いので、インストールしてから「これはできるっけ?」みたいな感じで機能を探すとよいです。

ivogabe/Brackets-Icons: File icons in Brackets' file tree

サイドバーなどに表示されるファイルのアイコンをいい感じにします。

似ているエクステンションは多くあるので、好みで選ぶとよいでしょう。

JeffryBooher/brackets-bookmarks-extension: Brackets Editor Bookmarks Extension

ソースコード内にブックマークをつけることができます。// TODOのようなコメントを残さずに済むようになります。

Wikunia/brackets-QuickDocsJS: Inline short documentation for JavaScript functions, including a summary,syntax and parameters.

クイックドキュメント(ショートカットキーで表示される説明)に、JSのドキュメントを追加してくれる。

sathyamoorthi/brackets-sidebar-plus: adding auto show/hide to sidebar

サイドバーの挙動を改善。

sprintr/brackets-color-palette: An extension for Brackets that lets you pick colors from images.

画像から色を抽出できるエクステンション。

ペイントを開いたり、インターネット上のツールを使ったりすることなく、Bracketsで完結できるようになります。

記事で紹介されていないエクステンション

AlanHohn/markdown-toolbar: Brackets extension that adds Markdown editing support via a toolbar

Markdown記法のツールバーです。ヘッダの挿入、リストの挿入、文字表現のON/OFF、リンクの作成などを簡便にしてくれます。

ranjandatta/brackets-open-in-linux-terminal: Open current location in terminal

Bracketsで開いているワークスペース内でターミナルを開くエクステンションです。

Nodejsの実行など、いちいちファイルエクスプローラをたどってターミナル・コマンドプロンプトを開くのが面倒くさかったので、Bracketsからすぐに開くために導入しました。

yasinkuyu/brackets-newdoc: Brackets new html5 document extension

HTML5ファイルの新規作成に利用するエクステンションです。

brackets-userland/brackets-git: brackets-git — git extension for adobe/brackets

BracketsにGitクライアントの機能を追加します。

差分の確認、ステージング、コミット・プッシュをGUIで実行できるようになります。ただし、差分の表示ではマルチバイト文字が文字化けします。

Bracketsの残念なところ

Bracketsの使いにくかったところ、残念だったところを上げていきます。

UTF-8以外の文字コードに対応していない

まずはこれですね。S-JISとかのファイルがあると別のエディタで開いて、変換してというようなことをしていました。これについてはエクステンション(Hiromi-Ayase/brackets-shizimily-multiencoding)を入れることで対応できるため、そこまで大きな問題ではないかもしれません。
ですが、エクステンションがマルチバイト文字に対応していないことがあり、これは時折問題になりました。Brackets-Gitは特に面倒くさく、差分を見てもマルチバイト文字が文字化けしてしまい、ろくに確認ができませんでした。

ファイル数が3万を超えるとフリーズする

プロジェクトとしてフォルダを開くとき、ファイルが3万を超えるとフリーズします。こちらもエクステンション(brackets-userland/brackets-file-tree-exclude)で対応できるそうですが、ちょっと不親切な感じもしました。

似たエクステンションを複数インストールするとクラッシュする

サイドバーの挙動を変更するエクステンションを複数インストールするとBracketsが起動しなくなったり、表示がおかしくなったりします。
当然といえば当然ですが、こっちのエクステンションのこの機能とあっちのエクステンションのこの機能がほしいというとき、諦めざるを得なかったのは残念です。

たぶんちょっと流行っていない

難癖っぽいですが、ちょっと流行っていなかったと思います。
エクステンションはあまり管理されていないものがあり、機能の追加やバグの修正が長く行なわれていないことがありました。

プラグインのインストールがプロキシに妨害される

会社で働くSEあるあるのプロキシ大嫌い問題です。 BracketsのエクステンションはAWSのS3からファイルをダウンロードし、PCに保存して利用するようになっているようです。社内プロキシは大抵の場合、このアクセスを許可してくれておらず、エクステンションを入れることができませんでした。(プロキシ設定をちゃんとすればできた??) やるとすればGitHubからzipファイルをダウンロード・解凍し、フォルダに配置でしょうか。

ちなみにVS Codeは利用者が多いから許可しているのか、プロキシ設定がちゃんと通るのか、実際にはわかりませんが。プラグインを入れられました。

Bracketsイチオシのライブビュー機能だが、VS CodeにもLive Serverという拡張機能が存在する

上記のようなことがあってVS Codeを使おうかなー、と思っていたところ、VS Codeにもライブビュー機能があることを知り、乗り換えることになりました。

ritwickdey/vscode-live-server: Launch a development local Server with live reload feature for static & dynamic pages.

おわりに

私自身、BracketsからVS Codeに乗り換えたため、今のPCにはBracketsのインストールすらしていないです。

HTML、CSSJavaScriptやNodejsを学び始めたときに、Bracketsを使っていたのは良い出会いだったとおもます。BracketsのエクステンションにGitHubでプルリクを出したこともありますし、思い入れのあるエディタです。
Bracketsの開発が別のサポートされるようになったり、後継となるエディタができたりしたら、また使ってみたいと思います。

サンキュー、Brackets

Amazon CloudFrontで「Operation would result in exceeding resource limits.」が発生した

Amazon CloudFrontを利用しているときに、「Operation would result in exceeding resource limits.」が発生したので、その原因について調べました。

最初に断っておくと、きちんとした解決はできずに諦めてしまいました。

原因

以下に同じエラーメッセージが発生したひとがおり、また原因についても参考先を示してくれていました。 https://github.com/aws-samples/aws-waf-sample/issues/8#issuecomment-298635439

内容をみるとCloudFront自体ではなく、CloudFrontに付与しているAWS WAFのせいということでした。

AWS WAFとは、Webアプリを保護するためのAWSが提供しているウェブアプリケーションファイアウォールのこと。SQLインジェクションXSS、また特定のIPアドレスからのアクセスを防ぐことができます。

さて、エラーの原因はWAFに設定されている制限(正式には「クォータ」)にひっかかっていることでした。下記のリンク先のいずれかのクォータのようですが、タイトルのようなエラーメッセージでは内容まではわかりませんでした。

今回は利用時ではなく、設定時に発生したので「ウェブ ACLs の最大数」かなと考えましたが、確認を取る方法は不明でした。

AWS WAF のクォータ

クォータは引き上げリクエストをおこない、承認されることで緩和されますが、若干時間がかかりそうですね。

(おわり...)

参考

AWS WAF を使用してコンテンツへのアクセスを管理する - Amazon CloudFront

Oracle OpenJDK15でOWASP ZAPが起動しない

『体系的に学ぶ 安全なWebアプリケーションの作り方 第2版』では、OWASP ZAPというアプリを使って学習を進めていきます。
インストールしたあと、ZAP.exeをダブルクリックしても起動しないことがありました。

原因と対処方法をまとめておきます。

続きを読む