Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 1654] ファイルの概要

このコミットは、Go言語の初期のドキュメンテーションサーバー(GDS: Go Doc Server)の機能強化に関するものです。具体的には、ディレクトリリスティング機能の追加、リンクの動作改善、ロギングの導入、およびパスのサニタイズ処理の強化が含まれています。これにより、GDSはより実用的なドキュメンテーション閲覧ツールへと進化しました。

コミット

commit e08cc15251416dfc63b043edd55e41078aa374fe
Author: Robert Griesemer <gri@golang.org>
Date:   Mon Feb 9 21:05:14 2009 -0800

    Some real GDS functionality:
    - directory listings w/ working links
    - some links working in source code (most don't do the right thing yet)
    - use of logging
    
    R=r
    OCL=24728
    CL=24728

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/e08cc15251416dfc63b043edd55e41078aa374fe

元コミット内容

Some real GDS functionality:
- directory listings w/ working links
- some links working in source code (most don't do the right thing yet)
- use of logging

R=r
OCL=24728
CL=24728

変更の背景

このコミットは、Go言語の初期開発段階において、Goソースコードのドキュメンテーションをブラウザ経由で提供するためのサーバー(GDS)の機能性を向上させることを目的としています。当時のGDSは基本的なファイル表示機能しか持たず、ディレクトリの内容を一覧表示したり、ソースコード内のリンクを適切に処理したりする能力が不足していました。

開発者は、Go言語のコードベースが拡大するにつれて、その構造を理解し、関連する定義や実装に素早くアクセスできるツールが必要であると認識していました。特に、Goの標準ライブラリやプロジェクト内の他のGoファイルへのナビゲーションは、開発効率に直結する重要な要素でした。

このコミット以前は、GDSは単一のGoファイルをコンパイルして表示する機能が主であり、ディレクトリ構造を視覚的に探索する手段がありませんでした。また、ソースコード内に記述されたパッケージ名などへのリンクも、適切に解決されていませんでした。これらの課題を解決し、GDSをより「実用的な」ドキュメンテーションツールにするために、本コミットが導入されました。ロギングの導入は、サーバーの動作状況を把握し、デバッグを容易にするための一般的な改善策です。

前提知識の解説

このコミットを理解するためには、以下の概念とGo言語の初期の設計に関する知識が役立ちます。

  1. Go言語の初期開発とツールチェイン:

    • Go言語は2009年にGoogleで公開されました。このコミットはその直後の非常に初期の段階のものであり、言語仕様や標準ライブラリ、ツールチェインがまだ活発に開発・変更されていた時期です。
    • 当時のGoの標準ライブラリやパッケージ構造は、現在のものとは異なる部分が多く存在します。例えば、logパッケージは現在とほぼ同じですが、osパッケージのAPIは現在とは異なる可能性があります。
    • httpパッケージも同様に、基本的な機能は備えていたものの、現在のnet/httpパッケージのような洗練されたミドルウェアやルーティングの概念はまだ確立されていなかったかもしれません。
  2. Go Doc Server (GDS):

    • Go言語には、ソースコードからドキュメンテーションを生成し、Webブラウザで閲覧可能にするgo docコマンドと、それをWebサーバーとして提供するgodocツールが存在します。このコミットで言及されているGDSは、godocツールの前身、あるいはそのプロトタイプのようなものと考えられます。
    • GDSの主な目的は、Goのソースコードを解析し、HTML形式で整形してブラウザに表示することです。これには、コードのシンタックスハイライト、定義へのリンク、パッケージ構造の表示などが含まれます。
  3. osパッケージとファイルシステム操作:

    • Goのosパッケージは、オペレーティングシステムとのインタラクション、特にファイルシステム操作(ファイルの読み書き、ディレクトリの作成・削除、ファイル情報の取得など)を提供します。
    • os.Openはファイルやディレクトリを開くために使用され、os.Readdirはディレクトリ内のエントリ(ファイルやサブディレクトリ)を読み取るために使用されます。
    • os.Dir構造体(現在のos.FileInfoインターフェースに相当)は、ファイル名、サイズ、パーミッション、ディレクトリかどうかなどの情報を提供します。
  4. net/httpパッケージとWebサーバー:

    • Goのnet/httpパッケージは、HTTPクライアントとサーバーの実装を提供します。
    • http.Handleは特定のURLパスに対するハンドラ関数を登録するために使用されます。
    • http.ListenAndServeは指定されたポートでHTTPサーバーを起動し、リクエストを処理します。
    • http.ConnはHTTP接続を表し、fmt.Fprintfなどを用いてクライアントに応答を書き込むために使用されます。
    • http.Requestは受信したHTTPリクエストの詳細(URL、ヘッダーなど)を含みます。
  5. sortパッケージ:

    • Goのsortパッケージは、スライスやカスタムコレクションをソートするための汎用的なインターフェースとアルゴリズムを提供します。
    • sort.Interfaceインターフェース(Len(), Less(i, j int), Swap(i, j int)の3つのメソッドを持つ)を実装することで、任意の型をソート可能にします。
  6. logパッケージ:

    • Goのlogパッケージは、簡単なロギング機能を提供します。log.Stdoutfは標準出力にフォーマットされた文字列を出力します。log.Exitfはメッセージを出力した後にプログラムを終了します。
  7. パスの正規化とサニタイズ:

    • Webアプリケーションにおいて、ユーザーからの入力パスを直接ファイルシステムパスとして使用することはセキュリティ上のリスク(ディレクトリトラバーサルなど)を伴います。そのため、パスを正規化(例: ///に、/../を適切に解決)し、安全な形式にサニタイズする処理は非常に重要です。

技術的詳細

このコミットは、GDSのWebサーバーとしての振る舞いを大きく変更し、ファイルシステム上のディレクトリ構造をWebブラウザで探索できるようにするものです。

  1. ディレクトリリスティング機能の追加 (serveDir関数):

    • serveDir関数が新しく導入されました。この関数は、指定されたディレクトリの内容を読み込み、HTML形式でブラウザに表示します。
    • os.Openos.Readdirを使用してディレクトリ内のエントリ(ファイルやサブディレクトリ)を取得します。
    • 取得したエントリは、新しく定義されたDirArray型(sort.Interfaceを実装)を使用して名前順にソートされます。これにより、ディレクトリリスティングがアルファベット順に表示され、ユーザーにとって見やすくなります。
    • 表示は3つのセクションに分けられます:
      1. ディレクトリ: サブディレクトリはリンクとして表示され、クリックするとそのサブディレクトリの内容に移動できます。
      2. .goファイル: Goソースファイルもリンクとして表示され、クリックするとそのGoファイルのコンパイル済み(整形済み)内容が表示されます。
      3. その他のファイル: Goファイルでもディレクトリでもないファイルは、リンクなしのグレーアウトされたテキストとして表示されます。これは、GDSが主にGoソースコードの閲覧に特化しているため、その他のファイルタイプは直接表示しないという設計思想を示しています。
    • printLinkヘルパー関数が導入され、HTMLの<a>タグを生成してリンクを簡単に作成できるようになりました。
  2. リクエストハンドリングの改善 (serve関数):

    • 以前のdocServer関数に代わり、serve関数がHTTPリクエストのメインハンドラとして機能します。
    • serve関数は、リクエストされたURLパスがディレクトリであるか、Goファイルであるかをos.StatisGoFile関数を使って判断します。
    • パスがディレクトリであればserveDirを呼び出し、GoファイルであればserveFile(旧docServer)を呼び出します。
    • それ以外の場合は、http.StatusNotFoundを返します。これにより、サーバーはGoソースコードとディレクトリの閲覧に特化し、他のファイルタイプへの直接アクセスを制限します。
  3. パスのサニタイズ (utils.gocleanPathSanitizePath):

    • usr/gri/pretty/utils.gocleanPathSanitizePathという新しい関数が追加されました。
    • cleanPathは、パス内の連続するスラッシュ(例: //, ///)を単一のスラッシュに正規化します。
    • SanitizePathcleanPathを呼び出した後、パスの末尾のスラッシュを削除します(ただし、ルートパス/の場合は削除しない)。
    • これらの関数は、ユーザーからのURLパス入力を安全に処理し、ファイルシステムパスとして使用する前に正規化するために導入されました。これはセキュリティとパスの一貫性を保つ上で非常に重要です。
  4. ロギングの導入:

    • logパッケージがインポートされ、fmt.Printfの代わりにlog.Stdoutfが冗長モード(-vフラグ)での出力に使用されるようになりました。
    • サーバー起動時のエラーハンドリングもpanicからlog.Exitfに変更され、より適切なエラー報告とプログラム終了が行われるようになりました。
  5. リンク生成の修正 (printer.go):

    • usr/gri/pretty/printer.go内のHTMLリンク生成ロジックが修正され、ハードコードされていたhttp://localhost:6060/gds/src/lib/のようなプレフィックスが/src/lib/に短縮されました。これは、gds.gohttp.Handle("/", ...)が設定されたことと整合性が取れており、サーバーがルートパスからコンテンツを提供するようになったことを反映しています。
  6. urlPrefixの削除:

    • 以前はurlPrefix = "/gds"という変数が存在し、URLパスの処理に使用されていましたが、新しいserve関数がルートパスからリクエストを処理するようになったため、この変数は不要となり削除されました。

コアとなるコードの変更箇所

usr/gri/pretty/gds.go

  • import"sort""log"が追加。
  • var urlPrefixが削除。
  • var rootの初期化がflag.Stringに変更され、コマンドライン引数でルートディレクトリを指定可能に。
  • getFilename関数が削除され、URLパスの処理ロジックがserve関数内に統合。
  • DirArray型(sort.Interface実装)が追加され、ディレクトリ内のエントリをソート可能に。
  • isGoFile関数が追加され、ファイルがGoソースファイルであるかを判定。
  • printLink関数が追加され、HTMLリンクの生成を簡素化。
  • serveDir関数が新しく追加され、ディレクトリの内容をHTMLで表示するロジックを実装。
  • docServer関数がserveFile関数に実質的に置き換えられ、ファイルコンパイルと表示ロジックを担当。
  • serve関数が新しく追加され、HTTPリクエストのルーティング(ディレクトリかGoファイルか)を担当。
  • main関数内で、ロギングにlog.Stdoutfを使用するよう変更。
  • main関数内で、http.Handle(urlPrefix + "/", ...)http.Handle("/", ...)に変更され、サーバーがルートパスからリクエストを処理するよう設定。
  • main関数内で、ルートディレクトリの存在チェックとサニタイズ処理が追加。

usr/gri/pretty/printer.go

  • HtmlPackageName関数内のリンク生成部分が変更され、http://localhost:6060/gds/src/lib/のような絶対パス指定が/src/lib/という相対パス指定に修正。

usr/gri/pretty/utils.go

  • cleanPath関数が追加され、パス内の連続するスラッシュを単一に正規化。
  • SanitizePath関数が追加され、cleanPathを呼び出した後、末尾のスラッシュを削除してパスをサニタイズ。

コアとなるコードの解説

usr/gri/pretty/gds.go

type DirArray []os.Dirfunc (p DirArray) Len() int, func (p DirArray) Less(i, j int) bool, func (p DirArray) Swap(i, j int)

これはGoのsortパッケージが提供するsort.Interfaceインターフェースの実装です。os.Dir(現在のos.FileInfoに相当)のスライスであるDirArrayを定義し、そのLenLessSwapメソッドを実装することで、sort.Sort(DirArray(list))のようにしてディレクトリ内のエントリを名前順にソートできるようになります。Lessメソッドはp[i].Name < p[j].Nameと定義されており、アルファベット順にソートされます。

func serveDir(c *http.Conn, dirname string)

この関数は、指定されたdirname(ディレクトリのパス)の内容を読み込み、HTTP接続cにHTML形式で出力します。

  1. os.Open(*root + dirname, os.O_RDONLY, 0): root(サーバーのルートディレクトリ)とdirnameを結合した絶対パスでディレクトリを開きます。
  2. エラーが発生した場合(ディレクトリが見つからないなど)は、http.StatusNotFoundを返し、エラーメッセージを出力します。
  3. os.Readdir(fd, -1): 開いたディレクトリfdから全てのエントリを読み込みます。
  4. sort.Sort(DirArray(list)): 読み込んだエントリのリストを名前順にソートします。
  5. c.SetHeader("content-type", "text/html; charset=utf-8"): レスポンスのContent-TypeをHTMLに設定します。
  6. fmt.Fprintf(c, "<b>%s</b>\n", path): 現在のディレクトリパスを太字で表示します。
  7. 3つのセクションでの表示:
    • ディレクトリ: entry.IsDirectory()でディレクトリを判別し、printLinkを使ってリンクとして表示します。
    • .goファイル: isGoFile(&entry)でGoファイルを判別し、printLinkを使ってリンクとして表示します。
    • その他のファイル: ディレクトリでもGoファイルでもないエントリは、fmt.Fprintf(c, "<font color=grey>%s</font><br>\n", entry.Name)でグレーアウトされたテキストとして表示されます。

func serve(c *http.Conn, req *http.Request)

この関数は、すべてのHTTPリクエストを処理するメインハンドラです。

  1. log.Stdoutf("URL = %s\n", req.RawUrl): 冗長モードの場合、リクエストされたURLをログに出力します。
  2. path := Utils.SanitizePath(req.Url.Path): リクエストURLのパス部分をUtils.SanitizePathでサニタイズします。これにより、パスの正規化とセキュリティが確保されます。
  3. dir, err := os.Stat(*root + path): サニタイズされたパスとルートディレクトリを結合した絶対パスに対してos.Statを実行し、ファイルまたはディレクトリの情報を取得します。
  4. エラーが発生した場合(ファイルやディレクトリが見つからないなど)は、http.StatusNotFoundを返し、エラーメッセージを出力します。
  5. switch文でdirのタイプをチェックし、適切なハンドラを呼び出します。
    • case dir.IsDirectory(): パスがディレクトリであればserveDir(c, path)を呼び出します。
    • case isGoFile(dir): パスがGoファイルであればserveFile(c, path)を呼び出します。
    • default: それ以外の場合は、http.StatusNotFoundを返します。

usr/gri/pretty/utils.go

func cleanPath(s string) string

この関数は、文字列s内の連続するスラッシュ(例: a//b, a///b)を単一のスラッシュに変換します。再帰的に実装されており、パスを走査して連続するスラッシュを見つけると、その部分を単一のスラッシュに置き換え、残りの部分に対して再帰的に処理を続けます。

func SanitizePath(s string) string

この関数は、パス文字列sを安全な形式にサニタイズします。

  1. s = cleanPath(s): まずcleanPathを呼び出して、連続するスラッシュを正規化します。
  2. if s[len(s)-1] == '/': パスの末尾がスラッシュであるかをチェックします。
  3. s = s[0 : len(s)-1]: 末尾にスラッシュがあれば削除します。ただし、これはルートパス/の場合には適用されません(len(s)-1が0になるため)。

これらの関数は、Webリクエストから受け取ったパスをファイルシステム操作に安全に利用するための重要な前処理です。

関連リンク

参考にした情報源リンク

  • Go言語の初期のコミット履歴 (GitHub): https://github.com/golang/go/commits/master
  • Go言語のgodocツールの歴史と機能に関する情報 (Goブログなど): https://go.dev/blog/godoc
  • Go言語のnet/httpパッケージの進化に関する情報 (Goブログ、GoのIssueトラッカーなど)
  • Go言語のosパッケージの進化に関する情報 (Goブログ、GoのIssueトラッカーなど)
  • 一般的なWebセキュリティにおけるパスのサニタイズに関する情報