うならぼ

どうも。

F#からFiddlerCoreを触る

リセマラ用にキャッシュプロキシでも作ってみようかと思ったんだ。

結局そこまで高速化はできなくて、そもそもリセマラ面倒になったよね。

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#らしくないというか、すごく手続き型っぽいコードですねえ。。