なうびるどいんぐ

脳みそ常時-3dB

git-lfsの仕様(サーバー側)を個人的に解説してみる

      2021/03/16    HimaJyun

以前、git-lfsがどのようなやり取りを行うのかを調べて実装してみた事があるので、その経験をもとにサーバー側の実装を個人的に解説してみようと思います。

あくまで個人的な解説なので情報の間違いを含む可能性もあります。「こうすれば動いた」くらいの情報。

スポンサーリンク

ちなみにこの記事は2016/08/04での情報です。恐らくlfs本体の進化に合わせて記事は古くなるでしょう、その頃には私より上手い解説を出来る人が現れるはず。

git-lfsのAPI

公式のドキュメントも併せて読む事を強く推奨。

git-lfsのサーバーはHTTPでやり取りを行います。gitがSSHでやり取りされていてもLFSはHTTPでやり取りします。

基本的に以下の手順でpushされます。

  1. SSHの場合のみ: git-lfs-authenticateコマンドでエンドポイントのURLを取得
  2. エンドポイントに対してjsonをPOSTし、アップロード/ダウンロード用のURLを取得
  3. アップロードの場合: PUTでアップロード / ダウンロードの場合: GETでダウンロード

HTTPのPUTやGETでやり取りする仕様から、ある種のオブジェクトストレージだと考えると分かりやすいでしょう。

LegacyAPIとBatchAPI

git-lfsにはLegacyAPIとBatchAPIの2種類があります。

が……LegacyAPIは名前の通りレガシーで、効率も悪いためBatchAPIを使った方が良い。

BatchAPIだけサポートしておけば十分です。(確かGitLabもBatchAPIしかサポートしていなかったかと)

jsonがPOSTされてくる

SSHの場合は最初にgit-lfs-authenticateコマンドでエンドポイントを取得する操作が入るのですが、これを理解するためには先にHTTPでの操作を理解した方が良いので解説は後回しにします。

HTTPの場合、まず最初にjsonがPOSTされて来ます。飛んでくるjsonはこんな感じ。

{
  "objects": [
    {
      "oid": "e5b844cc57f57094ea4585e235f36c78c1cd222262bb89d53c94dcb4d6b3e55d",
      "size": 10485760
    }
  ],
  "operation": "upload"
}

各項目の意味は次の通りです。

  • objects:操作を要求しているオブジェクトの配列
  • operation:ユーザが行いたい操作
  • oid:オブジェクト(データ)のSHA256ハッシュ
  • size:オブジェクトのサイズ

これらはユーザが送ってくる情報のため偽装されている可能性もあります。(つまり?: oidをSHA256だと思い込んで、そのままファイルパスとして扱うような雑な実装をしてしまうとディレクトリトラバーサルなどの脆弱性を生む)

ユーザーが送ってきた情報を過信せずに、サーバー側で検証しましょう。(認証情報は正しいか?リポジトリへの書き込み権限はあるか?要求の内容は適正か?などなど)

このjsonはPOSTのリクエストボディに含まれているので、それらを読み取ってjsonをパースする処理が必要です。

(つまり?: 使用する言語やフレームワークによってはWebでフォームを使う時とは異なる処理が必要な場合がある。例えばPHPだとphp://inputから読み取る必要がある)

注意点: ユーザーが送信してきたリクエストボディを検証もせずにjsonパーサーに食わせたりしてはいけない。(メモリを使い果たすタイプのDoS攻撃が可能になります) サーバー側でアップロードサイズ制限を掛ける、jsonパーサーに食わせる前にサイズを確認するなどの検証を行う事を強く推奨

複数ファイルの場合のjson

BatchAPIは1度に複数ファイルの情報を取得しようとします。

その場合のjsonは以下のようにります。

{
  "objects": [
    {
      "oid": "e5b844cc57f57094ea4585e235f36c78c1cd222262bb89d53c94dcb4d6b3e55d",
      "size": 10485760
    },
    {
      "oid": "0f6d7219c5295725e733b9910ae259ef98106a4bc7b388b513e707440f4da5cf",
      "size": 102401
    },
    {
      "oid": "f627ca4c2c322f15db26152df306bd4f983f0146409b81a4341b9b340c365a16",
      "size": 102400
    }
  ],
  "operation": "upload"
}

oidやsizeが増えるだけなので難しくはありませんね。

リクエストに対するレスポンス

サーバ側はユーザのリクエストを解釈して、次のようにレスポンスを送り返す必要があります。

{
  "objects": [
    {
      "oid": "e5b844cc57f57094ea4585e235f36c78c1cd222262bb89d53c94dcb4d6b3e55d",
      "size": 10485760,
      "actions": {
        "upload": {
          "href": "https://example.com/lfs/upload/hogehoge"
        },
        "download": {
          "href": "https://example.com/lfs/download/hogehoge"
        },
        "verify": {
          "href": "https://example.com/lfs/verify/hogehoge"
        }
      }
    },
    {
      "oid": "0f6d7219c5295725e733b9910ae259ef98106a4bc7b388b513e707440f4da5cf",
      "size": 102401,
      "actions": {
        "upload": {
          "href": "https://example.com/lfs/upload/?hoge=fuga",
          "header": {
            "Key": "value",
            "Authorization": "YWRtaW46MTIz"
          },
          "expires_at": "2015-07-27T21:15:01Z"
        }
      }
    }
  ]
}

oidとsizeはユーザーが要求してきたものをそのまま送り返したので良いでしょう。

actionsの中にユーザーが可能な操作(upload, download, verify)を含めます。ユーザの要求してきた操作に対するアクションがあれば良いです。(複数含まれている場合は必要な操作が利用される)

verifyアクションはupload操作と合わせて使われるらしく、アップロードが正常に完了したかをを確認するためのURLみたいです。(無くても動作します)

各アクションの中にはhref、header、expires_atの3つが利用可能で、以下のような意味を持ちます。

  • href: この操作を行うためのURL、ここで教えたURLにアクセスしてくる。クエリ文字などで追加の情報を含めてもよし
  • header: ここで指定した物がヘッダとして送られてくる。ワンタイムトークンやBasic認証の情報を渡してやればユーザー認証の回数を減らせる。(もちろん他にも情報を含めてもよし)
  • expires_at: 操作が有効な期間、トークン型の認証を利用する際などに利用する

jsonの一部にエラーがあった場合

jsonそのもののエラー(フォーマットが不正、等)の場合は後述の「エラー時のレスポンス」をご覧下さい。

送られてきた要求の中に不正な値(認証されていないとか、データが壊れてるとか)があった場合には次のようにしてエラーを通知します。

{
  "objects": [
    {
      "oid": "123",
      "size": -1,
      "error": {
        "code": 400,
        "message": "Bad Resuet"
      }
    }
  ]
}

messageに日本語を含めることも可能だと思いますが、文字コードの関係もあるのであまり推奨はしない。

LFSのURLが異なる場合

SSHの場合はgit-lfs-authenticateコマンドでgit-lfsのURLを教える事ができるのですが、HTTPの場合はデフォルトだとGitのリモートURLにそのまま取りに来ようとします。

これはGitのURLとLFSのURLが異なる場合に問題になります。

例えば、URLがhttps://example.com/hoge.gitだった場合、pushやcloneを行うとhttps://example.com/hoge.git/info/lfs/objects/batchに対してjsonが送られてきます。

(つまり?: GitサーバーがLFSに対応していなかったり、サーバーが異なる=ファイルが存在しなかったりすると、そのまま404になってcloneがコケます)

この問題の対処法は簡単で、次のようなコマンドを実行して作成される.lfsconfigファイルをリポジトリに入れておけばOKです。

git config -f .lfsconfig lfs.url <LFSのURL>

(昔は.lfsconfigとか出来なかったはずだと記憶していたのですが、今調べ直してみると割と最初期からこの仕様はある模様)

エラー時のレスポンス

次に進む前にエラー時のレスポンスを説明しておきましょう。

エラー処理はjsonをPOSTしてきた場合に限らず、PUTでのアップロードやGETでのダウンロードに対しても可能みたいです。

レスポンスコード

git-lfsは次のレスポンスコードを認識します。

  • 200: リクエストは正常に処理されました。
  • 202: 要求が受け入れられている、クライアントはURLを辿る必要がある (jsonをPOSTした時用?)
  • 301: 恒久的なリダイレクト、GETとHEADのリクエストに対してしか使えない
  • 302: 一時的なリダイレクト、GETとHEADのリクエストに対してしか使えない
  • 303: 一時的なリダイレクト、GETとHEADのリクエストに対してしか使えない
  • 307: 一時的なリダイレクト、元の要求メソッドを保持したままリダイレクトされる。
  • 400: 一般エラー、例えばjsonが不正だとか
  • 401: 認証が必要、資格情報が正しくない
  • 403: ユーザはレポジトリに対する読み取り権限を持つが、書き込み権限がない
  • 404: 存在しないレポジトリ、またはファイルに対しての読み取り権限を持たない
  • 406: 受理出来ない、主にAcceptヘッダがapplication/vnd.git-lfs+jsonでない場合に使用
  • 410: 元々は存在したけど、後から消された。理由なども送る必要あり?(ユーザが消した、検閲、など)
  • 413: BatchAPIに含まれるオブジェクトが多すぎるか、リクエストサイズが大きすぎる
  • 422: 要求されたオブジェクトで1つ以上のエラー、アップロード要求(POSTしてきたjson)が不正な場合などに使用
  • 429: レート制限に達した (おそらくAPI読み出しのレート制限だと思われる)
  • 501: サーバは現在この機能を提供していない、将来のために予約されている。
  • 507: ストレージ不足 (容量制限などでもこれを使うっぽい?)
  • 509: 帯域幅の制限を超えている (転送帯域の制限)

エラーメッセージの送信

エラーが発生した場合に以下のようなjsonを送り返すことでユーザに任意のメッセージを表示させられます。

{
  "message": "エラーですよん。",
  "documentation_url": "https://example.com/help/errors",
  "request_id": "123"
}

messageはエラーの内容でクライアントに表示されます。日本語も表示できるみたいですが、環境依存でしょうから英語にしておくべき。

documentation_urlはエラーに関する情報が載ったページのURL (日本語での情報を表示したい場合はここで日本語ページのURLを提示する方が良いでしょう)

request_idはリクエストを識別する一意のID

documentation_urlとrequest_idは無くても可。

各アクションの処理

jsonのパースが終わったら次に来るのはバカでかいファイルのアップロードか、バカでかいファイルのダウンロードです。(稀にVerification)

巨大なファイルである事を意識して処理しないとメモリを食いつぶす可能性があるので要注意。

アップロードやダウンロードの前にユーザー権限の検証をお忘れなく。

アップロード処理

PUTを利用してリクエストボディにバイナリがどんぶらこと乗ってきます。

ファイル操作系APIを使用して少しずつ書き込んでいくやり方がメインになるはず。

間違ってもリクエストボディをメモリに展開したりしてはいけない。(とくにPHPとかは油断するとこういう操作が簡単に出来てしまうので要注意)

Webサーバー側の設定も意識した方が良いでしょう。巨大なファイルをアップロードする時のチューニングとかもあるので。

ダウンロード処理

こちらはファイルを送るだけなので簡単ですね。間に入っているWebサーバーなどによってはここで出力バッファが掛かっていたりするので、その辺の調整も気にしておくと良いでしょう。

WebサーバーとしてApacheを利用している場合はX-Sendfileを、nginxを使用している場合はX-Accel-Redirectを使うという手もあります。(それ以外のWebサーバーでも類似の機能があります)

Verification処理

ぶっちゃけこれを実装したことはないので分かりませんが、ファイルがサーバ上に存在するか確認するための機能みたいです。

設定してある場合には次のようなjsonがPOSTされてきます。

{"oid": "0f6d7219c5295725e733b9910ae259ef98106a4bc7b388b513e707440f4da5cf", "size": 102401}

指定されたファイルがサーバに存在する場合は200 OKを、存在しない場合は404 Not Foundなどを返せば良いかと思われます。

git-lfs-authenticateコマンド

最後にこいつ。

SSHでgitのpushやcloneを行う場合はHTTPの(LFSサーバの)URLを確認する必要があります。(SSHでそのままファイルを渡せば良いのに……と思わなくはない)

クライアントがサーバに対して実行するコマンドは次のようになっています。

git-lfs-authenticate hoge/fuga.git download

git-lfs-authenticateコマンドの引数としてレポジトリ名とアクションが送られてくるわけです。

レスポンス

やはりjsonで返す模様、以下のようなjsonを返せば良いです。

{
  "header": {
    "Key": "value"
  },
  "href": "https://example.com/hoge.git/info/lfs"
}

hrefではURLを教えてやります、このURLはjsonをPOSTするためのURLです。HTTPであればinfo/lfsのような形で指定されていた物です。(objects/batchは勝手に付け加えられるので指定してはいけない)

headerに指定された値はリクエストヘッダにつけられて送られてきます。

後はこのhrefにHTTPでjsonがポストされてきて、actionを返して……という操作になります。

エラー時

エラーの内容を示すメッセージを標準エラー出力(STDERR)に出せば良いみたいです。

ただ単にURLを教えてやるだけなのであまり細かい事をする必要はないでしょう。

終わりに

そこまで複雑な仕組みでもありませんね。ある種のオブジェクトストレージみたいなものです。

認証、検証、巨大ファイルの扱いさえちゃんとやれば後はそこまで高度な技術力も要求されません。

既存の実装で気に入ったものが存在しないのであれば自分で実装するのもありですよ。

 - プログラミング