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 <- 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;
}
]