Webmentions Request Verification

Description

Sample script that shows how to perform Webmention request verification per Webmentions specification.

Usage

dotnet fsi request-verification.fsx

Snippet

request-verification.fsx

// https://www.w3.org/TR/webmention/#request-verification

// 1. Send response with 202 Accepted to acknowledge successful request // 2. DONE: Check that the protocol is http or https // 3. DONE: Source URL is different than Target URL // 4. DONE Check that Target URL is a valid resource

#r "nuget: Microsoft.AspNetCore.WebUtilities, 2.2.0"

open System open System.Net open System.Net.Http open System.Collections.Generic open Microsoft.AspNetCore.WebUtilities

type RequestVerificationResult = | Ok of HttpRequestMessage | Error of string

// Parse Form URL Encoded string let getFormContent (request:HttpRequestMessage) = async { let! content = request.Content.ReadAsStringAsync() |> Async.AwaitTask let query = QueryHelpers.ParseQuery(content) let source = query["source"] |> Seq.head let target = query["target"] |> Seq.head

    return source,target
}

// Check protocol is HTTP or HTTPS let checkProtocol (request: RequestVerificationResult) = match request with | Ok m -> let source,target = async { return! getFormContent(m) } |> Async.RunSynchronously

    let isProtocolValid = 
        match source.StartsWith("http"),target.StartsWith("http") with
        | true,true -> Ok m
        | true,false -> Error "Target invalid protocol"
        | false,true ->  Error "Source invalid protocol"
        | false,false -> Error "Source and Target invalid protocol"

    isProtocolValid
| Error s -> Error $"{s}"

// Check the URLs are not the same let checkUrlsSame (request:RequestVerificationResult) = match request with | Ok m -> let source,target = async { return! getFormContent(m) } |> Async.RunSynchronously let check = match source.Equals(target) with | true -> Error "Urls are the same" | false -> Ok m check | Error s -> Error s

// Helper functions let uriIsMine (url:string) = let uri = new Uri(url) uri.Host.Equals("lqdev.me") || uri.Host.Equals("www.luisquintanilla.me") || uri.Host.Equals("luisquintanilla.me")

let isValid (url:string) (msg:HttpResponseMessage) = let isMine = uriIsMine url isMine & msg.IsSuccessStatusCode

// Check URL is a valid resource // Valid means, the URL is one of my domains and returns a non-400 or 500 HTML status code let checkUrlValidResource (request:RequestVerificationResult) = match request with | Ok m -> let res = async { let! source,target = getFormContent(m) use client = new HttpClient() let reqMessage = new HttpRequestMessage(HttpMethod.Head, target) let! resp = client.SendAsync(reqMessage) |> Async.AwaitTask return isValid target resp } |> Async.RunSynchronously match res with | true -> Ok m | false -> Error "Target is not a valid resource" | Error s -> Error s

// Combine validation steps into single function let validate = checkProtocol >> checkUrlsSame >> checkUrlValidResource

// Test application let buildSampleRequestMessages (content:IDictionary<string,string>) =

let reqMessage = new HttpRequestMessage()
reqMessage.Content &lt;- new FormUrlEncodedContent(content)

let liftedReqMessage = Ok reqMessage
liftedReqMessage

let sampleContent = [ dict [ ("source","http://lqdev.me") ("target","http://lqdev.me") ] dict [ ("source","http://://lqdev.me") ("target","protocol://lqdev.me") ]
dict [ ("source","http://lqdev.me") ("target","http://github.com/lqdev") ] dict [ ("source","http://github.com/lqdev") ("target","http://lqdev.me") ]
]

sampleContent |> List.map(buildSampleRequestMessages) |> List.map(validate)

Sample Output

[  
  Error "Urls are the same"; 
  Error "Target invalid protocol";
  Error "Target is not a valid resource";
  Ok
    Method: GET, RequestUri: '<null>', Version: 1.1, Content: System.Net.Http.FormUrlEncodedContent, Headers:
    {
        Content-Type: application/x-www-form-urlencoded
        Content-Length: 67
    }
    {
        Content = System.Net.Http.FormUrlEncodedContent;
        Headers = seq [];
        Method = GET;
        Options = seq [];
        Properties = seq [];
        RequestUri = null;
        Version = 1.1;
        VersionPolicy = RequestVersionOrLower;
    }
]