リセマラ用にキャッシュプロキシでも作ってみようかと思ったんだ。
結局そこまで高速化はできなくて、そもそもリセマラ面倒になったよね。
SAZの読み書きを実装してみる
FiddlerではセッションをSAZという形式で保存することができます。FiddlerCoreでもこれを読み書きすることはできるのですが、Zipアーカイブの処理は自分で実装する必要があります。
例によってろくなドキュメントがないので、SampleAppのSAZ-DotNetZip.csを参考に実装していきます。その名の通りサンプルはDotNetZipを使っていますが、今回はSystem.IO.Compression.ZipFileを使います。
open System open Fiddler open System.IO open System.IO.Compression type SAZWriter(zipname) = let zip = ZipFile.Open(zipname, ZipArchiveMode.Create) interface ISAZWriter with member self.Filename = zipname member self.Comment with set(v) = () // これは必ず呼ばれるので例外を投げられない member self.EncryptionMethod = raise <| NotSupportedException() member self.EncryptionStrength = raise <| NotSupportedException() member self.SetPassword(password) = raise <| NotSupportedException() member self.AddFile(filename, writer) = use stream = zip.CreateEntry(filename).Open() writer.Invoke(stream) member self.CompleteArchive() = zip.Dispose() true type SAZReader(zipname) = let zip = ZipFile.OpenRead(zipname) interface ISAZReader with member self.Filename = zipname member self.Comment = raise <| NotSupportedException() member self.EncryptionMethod = raise <| NotSupportedException() member self.EncryptionStrength = raise <| NotSupportedException() member self.Close() = zip.Dispose() member self.GetRequestFileList() = seq { for x in zip.Entries -> x.FullName } |> Seq.filter (fun x -> x.StartsWith("raw/") && x.EndsWith("_c.txt")) |> Seq.toArray member self.GetFileStream(filename) = zip.GetEntry(filename).Open() member self.GetFileBytes(filename) = use src = zip.GetEntry(filename).Open() use buf = new MemoryStream() src.CopyTo(buf) buf.ToArray() type SAZProvider() = interface ISAZProvider with member self.BufferLocally = false member self.SupportsEncryption = false member self.CreateSAZ(zipname) = new SAZWriter(zipname) :> ISAZWriter member self.LoadSAZ(zipname) = new SAZReader(zipname) :> ISAZReader [<EntryPoint>] let main argv = FiddlerApplication.OnNotification.Add (fun e -> printfn "%s" e.NotifyString |> ignore) FiddlerApplication.oSAZProvider <- new SAZProvider() let sessions = Utilities.ReadSessionArchive(@"r:\archive.saz", true) printfn "%d sessions loaded" sessions.Length Utilities.WriteSessionArchive(@"r:\archive2.saz", sessions, null, true) 0
内包表記とパイプライン演算子GetRequestFileListで内包表記やパイプライン演算子を使っていますが、内包表記ひとつで済ますこともできます。
member self.GetRequestFileList() = [| for x in zip.Entries do let name = x.FullName if name.StartsWith("raw/") && name.EndsWith("_c.txt") then yield name |]
内包表記なしでも。
member self.GetRequestFileList() = zip.Entries |> Seq.map (fun x -> x.FullName) |> Seq.filter (fun x -> x.StartsWith("raw/") && x.EndsWith("_c.txt")) |> Seq.take 1 |> Seq.toArray
パイプライン使うのはLINQで見慣れてるし、内包表記もラムダの嵐を避けられて悪くないし・・・と思った結果がさっきのコードです。中途半端かもしれない。
もうひとつF#らしいところというと、GetFileBytesで使っているuse演算子でしょうか。C#でいうusingですが、インデントが深くならなくて素敵。
プロキシを立てる
SAZを読み込んでURLが一致するものにキャッシュから返そうかと思ったんですが、数が多いとSAZの読み書きが遅いとか、Fiddler使うの面倒とか・・・そんなわけでhttpで取得されるあらゆるファイルをURLに対応したパスにキャッシュするという雑な実装に。
open System open Fiddler open System.IO let port = 1601 let cachePathBase = @"R:\response\" let cachePathFor (session: Session) = let uri = new Uri(session.fullUrl) if uri.LocalPath.EndsWith("/") then None else Some(Path.Combine(cachePathBase, uri.Host, uri.LocalPath.Substring(1))) let onBeforeRequest (sess: Session) = match cachePathFor sess with | Some cachePath -> if (File.Exists(cachePath)) then sess.Ignore() sess.LoadResponseFromFile(cachePath) |> ignore printfn "hit: %s" sess.url |> ignore else sess.Tag <- cachePath printfn "save: %s" sess.url |> ignore | None -> sess.Ignore() let onBeforeResponse (sess: Session) = sess.SaveResponse(sess.Tag :?> string, false) [<EntryPoint>] let main argv = CONFIG.IgnoreServerCertErrors <- true FiddlerApplication.OnNotification.Add <| fun e -> Console.WriteLine e.NotifyString FiddlerApplication.Prefs.SetBoolPref("fiddler.network.streaming.abortifclientaborts", true) FiddlerApplication.add_BeforeRequest <| new SessionStateHandler(onBeforeRequest) FiddlerApplication.add_BeforeResponse <| new SessionStateHandler(onBeforeResponse) FiddlerApplication.Startup (port, FiddlerCoreStartupFlags.Default &&& ~~~FiddlerCoreStartupFlags.RegisterAsSystemProxy &&& ~~~FiddlerCoreStartupFlags.DecryptSSL) printfn "Port: %d\nPress Enter to exit." port stdin.ReadLine() |> ignore 0
FiddlerCoreではおなじみのBeforeRequestイベントですが、 BeforeRequest.Add (fun e -> ...)
とはできません。こんなエラーが出ます。
イベント
BeforeRequest
が標準以外の型です。このイベントが別の CLI 言語で宣言された場合、イベントにアクセスするには、このイベントに明示的な add_BeforeRequest メソッドや remove_BeforeRequest メソッドを使用する必要があります。このイベントが F# で宣言された場合、イベントの型をIDelegateEvent<_>
またはIEvent<_,_>
のインスタンス化にします。
どうやら EventHandler<T>
でないことが問題のようです。自分でSessionStateHandler型のインスタンスを作って、add_BeforeRequest
に渡さないといけません。
あとはoption使ってる以外はF#らしくないというか、すごく手続き型っぽいコードですねえ。。