I would like to create a WebPart that forwards all requests to another web server that I specify.
Usage might look like this:
let app =
choose
[
path "/" >=> OK "Hello, world. "
path "/graphql" >=> createProxy "localhost:5000"
RequestErrors.NOT_FOUND "Not found"
]
startWebServer defaultConfig app
How should I implement this in Suave?
I found this snippet, but it seems to be for an old version of Suave:
let build_proxy_resolver (fwd_to_host : String) fwd_to_port =
let heserver = System.Net.Dns.GetHostEntry(fwd_to_host)
let ipaddr = heserver.AddressList.[0]
fun (request : HttpRequest) ->
Some (ipaddr, fwd_to_port)
let build_headers ctx =
//add and remove headers from the ctx, return the header list
ctx.request.headers
let proxy_app (ctx:HttpContext) =
let new_headers = build_headers ctx
let fwd_ctx = {ctx with request={ctx.request with headers=new_headers}}
let pxy = proxy (build_proxy_resolver "PROXY_TO.com" 80us) fwd_ctx
{ctx with response = { ctx.response with status=Suave.Types.Codes.HTTP_200; content=SocketTask pxy }} |> Some
This has now been added to Suave:
open System
open Suave
open Suave.Proxy
let app =
choose
[
path "/" >=> OK "Hello, world. "
path "/graphql" >=> proxy (Uri "http://localhost:5000")
RequestErrors.NOT_FOUND "Not found"
]
startWebServer defaultConfig app
Related
I am using JWT(System.IdentityModel.Tokens.Jwt) for authentication in Giraffe F#. I am successfully getting the JWT token, but when I use it on my API/tables route I get a 401 unauthorized response. I have tried to change the issuer and audience to http://localhost:5001 and http://localhost:5000 respectively that didn't work then I disabled ValidateAudience but didn't work either.
Here is my route:
open System
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Cors.Infrastructure
open Microsoft.AspNetCore.Hosting
open Microsoft.Extensions.Hosting
open Microsoft.Extensions.Logging
open Microsoft.Extensions.DependencyInjection
open Giraffe
open backend.HttpHandlers
open System.Text
open Microsoft.IdentityModel.Tokens
open Microsoft.AspNetCore.Authentication.JwtBearer
open Microsoft.AspNetCore.Http
open JwtAuth
let authorize : HttpFunc -> HttpContext -> HttpFuncResult =
requiresAuthentication (challenge JwtBearerDefaults.AuthenticationScheme)
let webApp =
choose [ subRoute
"/api"
(choose [ POST
>=> choose [ route "/token" >=> Auth.handlePostToken
route "/user/add" >=> handleAddUser
route "/table/create">=> handleAddTable
route "/table/addData">=> authorize>=> handleAddTableData ]
GET
>=> choose [
route "/tables">=> authorize>=> handleGetTableNames ] ])
setStatusCode 404 >=> text "Not Found" ]
// ---------------------------------
// Error handler
// ---------------------------------
let errorHandler (ex: Exception) (logger: ILogger) =
logger.LogError(ex, "An unhandled exception has occurred while executing the request.")
clearResponse
>=> setStatusCode 500
>=> text ex.Message
Here is my config:
// ---------------------------------
// Config and Main
// ---------------------------------
let configureCors (builder: CorsPolicyBuilder) =
builder
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
|> ignore
let configureApp (app: IApplicationBuilder) =
let env =
app.ApplicationServices.GetService<IWebHostEnvironment>()
(match env.IsDevelopment() with
| true -> app.UseDeveloperExceptionPage()
| false ->
app.UseCors(configureCors)
.UseAuthentication()
.UseGiraffeErrorHandler(errorHandler)
.UseHttpsRedirection())
.UseGiraffe(webApp)
let configureServices (services: IServiceCollection) =
let sp = services.BuildServiceProvider()
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(fun options ->
options.TokenValidationParameters <-
TokenValidationParameters(
ValidateActor = false,
ValidateAudience = false,
ValidateLifetime = false,
ValidateIssuerSigningKey = false,
ValidIssuer = "http://localhost:5001",
ValidAudience = "http://localhost:5000",
IssuerSigningKey = SymmetricSecurityKey(Encoding.UTF8.GetBytes(Auth.secret))
))|> ignore
services.AddCors() |> ignore
services.AddGiraffe() |> ignore
let configureLogging (builder: ILoggingBuilder) =
builder.AddConsole().AddDebug() |> ignore
[<EntryPoint>]
let main args =
Host
.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(fun webHostBuilder ->
webHostBuilder
.Configure(Action<IApplicationBuilder> configureApp)
.ConfigureServices(configureServices)
.ConfigureLogging(configureLogging)
|> ignore)
.Build()
.Run()
0
Here is the function I use to generate the token:
let secret = "spadR2dre#u-ruBrE#TepA&*Uf#U"
let generateToken email =
let claims = [|
Claim(JwtRegisteredClaimNames.Sub, email);
Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) |]
let expires = Nullable(DateTime.UtcNow.AddHours(1.0))
let notBefore = Nullable(DateTime.UtcNow)
let securityKey = SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret))
let signingCredentials = SigningCredentials(key = securityKey, algorithm = SecurityAlgorithms.HmacSha256)
let token =
JwtSecurityToken(
issuer = "http://localhost:5001",
audience = "http://localhost:5000",
claims = claims,
expires = expires,
notBefore = notBefore,
signingCredentials = signingCredentials)
let tokenResult = {
Token = JwtSecurityTokenHandler().WriteToken(token)
}
tokenResult
I am using thunder client vscode to make the requests.
How can I redirect a connection from http to https using Suave?
at https://gist.github.com/ademar/f4ddb788162dbdd9e104574e2accf07f I found this:
let redirectToSsl : WebPart =
context(fun c ->
match c.request.header "x-forwarded-proto" with
| Choice1Of2 "http" ->
let uriBuilder = new UriBuilder(
Scheme = Uri.UriSchemeHttps,
Path = c.request.path,
Host = c.request.host)
Redirection.redirect (uriBuilder.Uri.ToString())
| _ -> fun _ ->async { return None })
but I am not really sure where that would fit in the pipeline?
I would change two things:
Check for the actual protocol. I think that x-forwarded-proto is only used for proxies, but I'm not certain.
To fit it into your pipeline, accept a webpart to invoke when the access is secure.
Result looks like this:
let redirectToSsl allow : WebPart =
context (fun c ->
if c.request.binding.scheme.secure then
allow
else
let uriBuilder =
new UriBuilder(
Scheme = Uri.UriSchemeHttps,
Path = c.request.path,
Host = c.request.host)
Redirection.redirect (uriBuilder.Uri.ToString()))
Usage looks like this:
let app = redirectToSsl Files.browseHome // allow browsing under SSL only
Caveat: I haven't tried this in practice, so there could be other issues I'm overlooking.
I have an existing owin application written in C# and would like to mount a suave application as a middleware but since I am relatively new to F# I am finding it quite difficult to navigate how this should be done. I think I'm looking for something like:
// in F# land
module MySuaveApp.ApiModule
let app =
choose
[ GET >=> choose
[ path "/hello" >=> OK "Hello GET"
path "/goodbye" >=> OK "Good bye GET" ]
POST >=> choose
[ path "/hello" >=> OK "Hello POST"
path "/goodbye" >=> OK "Good bye POST" ] ]
let getSuaveAsMiddleware() =
... magic goes here ...
// in Startup.cs
app.Use(MySuaveApp.ApiModule.getSuaveAsMiddleware())
As for what that magic should be I think it's a combination of OwinApp.ofAppFunc or OwinApp.ofMidFunc, but I can't for the life of me figure out what it should be.
There is no easy magic.1 ofAppFunc and ofMidFunc are here for creating WebParts out of OWIN components, i.e. OWIN -> Suave, whereas you want Suave -> OWIN.
The following works for your 'application' and serves as an example what would be needed to get it working:
open System.Runtime.CompilerServices
[<Extension>]
module Api =
open Suave
open Successful
open Filters
open Operators
open Microsoft.Owin
open System.Threading.Tasks
let app =
choose [ GET >=> choose [ path "/hello" >=> OK "Hello GET"
path "/goodbye" >=> OK "Good bye GET" ]
POST >=> choose [ path "/hello" >=> OK "Hello POST"
path "/goodbye" >=> OK "Good bye POST" ] ]
let withCtx (ctx : IOwinContext) webpart =
async {
let request =
{ HttpRequest.empty with
headers = ctx.Request.Headers |> List.ofSeq |> List.map (fun kvp -> kvp.Key, kvp.Value |> String.concat ",")
host = ctx.Request.Host.Value
``method`` = HttpMethod.parse ctx.Request.Method
url = ctx.Request.Uri }
let! res = webpart { HttpContext.empty with request = request }
res |> Option.iter (fun r ->
ctx.Response.StatusCode <- r.response.status.code
match r.response.content with
| Bytes bs -> ctx.Response.Write bs
| _ -> failwith "Not supported")
return res
}
type SuaveMiddleware(n) =
inherit OwinMiddleware(n)
override __.Invoke(context : IOwinContext) =
let res = withCtx context app |> Async.RunSynchronously
match res with
| Some _ -> Task.CompletedTask
| None -> base.Next.Invoke context
[<Extension>]
let UseSuave(app : Owin.IAppBuilder) =
app.Use(typeof<SuaveMiddleware>)
The main works is delegated to withCtx that tries to fulfill a request given a IOwinContext and a WebPart. It does so mainly by converting back and forth between Suave and OWIN context and related entities.
Note that this code is a PoC (Proof-of-Concept) and not fit for production.
The SuaveMiddleware forwards request to the next middleware if Suave cannot fulfill the request.
Using from C# is easy then:
using MySuave;
using Owin;
namespace Main
{
using System.Web.Http;
public class Startup
{
public static void Configuration(IAppBuilder appBuilder)
{
appBuilder.UseSuave();
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
appBuilder.UseWebApi(config);
}
}
}
given
namespace Main.Example
{
using System.Web.Http;
[RoutePrefix("api")]
public class ExampleController : ApiController
{
[HttpGet, Route("")]
public string Index()
{
return "Hello World";
}
}
}
And both URLs work:
http://localhost:9000/hello
Hello GET
http://localhost:9000/api
<string xmlns="http://schemas.microsoft.com/2003/10/Serialization/">Hello World</string>
1 At least none I know of. I'm happy to be proven wrong.
I am working on Suave 1.0 + Angular 2.0 sample app and very interesting to start Suave server in watch mode, so the server watch file changes (js,css,html) in root folder and sub-folders and automatically send refresh command to all open browser tabs with my application when any file is changed.
lite-server from Angular 2 5min Quckstark can do this and it is very handy.
I think that most of watch pieces can be found in latest Steffen Forkmann's post but it is not very clean how to send refresh to the open browser tabs.
Please provide complete code of similar implementation with Suave.
The code of Suave server should looks similar to this
#r "packages/Suave/lib/net40/suave.dll"
#r "packages/FAKE/tools/FakeLib.dll"
open Fake
open Suave
open Suave.Operators
open Suave.Sockets.Control
open Suave.WebSocket
open Suave.Utils
open Suave.Files
open Suave.RequestErrors
open Suave.Filters
open System
open System.Net
let port =
let rec findPort port =
let portIsTaken =
System.Net.NetworkInformation.IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpListeners()
|> Seq.exists (fun x -> x.Port = int(port))
if portIsTaken then findPort (port + 1us) else port
findPort 8083us
let logger = Logging.Loggers.ConsoleWindowLogger Logging.LogLevel.Verbose
let refreshEvent = new Event<_>()
let handleWatcherEvents (events:FileChange seq) =
for e in events do
let fi = fileInfo e.FullPath
traceImportant <| sprintf "%s was changed." fi.Name
refreshEvent.Trigger()
let socketHandler (webSocket : WebSocket) =
fun cx -> socket {
while true do
let! refreshed =
Control.Async.AwaitEvent(refreshEvent.Publish)
|> Suave.Sockets.SocketOp.ofAsync
do! webSocket.send Text (ASCII.bytes "refreshed") true
}
let cfg =
{ defaultConfig with
homeFolder = Some (__SOURCE_DIRECTORY__)
bindings =
[ HttpBinding.mk HTTP IPAddress.Loopback port ]
listenTimeout = TimeSpan.FromMilliseconds 3000. }
let app : WebPart =
choose [
Filters.log logger logFormat >=> never
Filters.path "/websocket" >=> handShake socketHandler
Filters.GET >=> Filters.path "/" >=> file "index.html"
Writers.setHeader "Cache-Control" "no-cache, no-store, must-revalidate"
>=> Writers.setHeader "Pragma" "no-cache"
>=> Writers.setHeader "Expires" "0"
>=> browseHome
NOT_FOUND "Found no handlers."
]
let watcher =
!! ("app/*.js")
++ ("*.html")
|> WatchChanges handleWatcherEvents
try
System.Diagnostics.Process.Start(sprintf "http://localhost:%d/index.html" port) |> ignore
startWebServer cfg app
finally
watcher.Dispose()
So we setup watcher that handle changes in js(generated by TypeScript) and html files and send refresh command to the client, but in the same time we need to add following code to the head section of index.html to handle refresh on the client side
<!-- 3. Listen on refresh events from the server -->
<script language="javascript" type="text/javascript">
function init()
{
websocket = new WebSocket("ws://"+window.location.host+"/websocket");
websocket.onmessage = function(evt) { location.reload(); };
}
window.addEventListener("load", init, false);
</script>
The full demo app you can find here
EDIT for moderators
I had this issue this morning, but the problem has been somehow solved on its own. If it were to come back and I could exactly tell what is happening I would reopen another question with more details.
Thx
I have the following code to start a http listener (I have so far copied and pasted a lot from this series of article )
httpAgent.fs :
namespace Server.Core
open System.Net
open System.Threading
type Agent<'T> = MailboxProcessor<'T>
/// HttpAgent that listens for HTTP requests and handles
/// them using the function provided to the Start method
type HttpAgent private (url, f) as this =
let tokenSource = new CancellationTokenSource()
let agent = Agent.Start((fun _ -> f this), tokenSource.Token)
let server = async {
use listener = new HttpListener()
listener.Prefixes.Add(url)
listener.Start()
while true do
let! context = listener.AsyncGetContext()
agent.Post(context) }
do Async.Start(server, cancellationToken = tokenSource.Token)
/// Asynchronously waits for the next incomming HTTP request
/// The method should only be used from the body of the agent
member x.Receive(?timeout) = agent.Receive(?timeout = timeout)
/// Stops the HTTP server and releases the TCP connection
member x.Stop() = tokenSource.Cancel()
/// Starts new HTTP server on the specified URL. The specified
/// function represents computation running inside the agent.
static member Start(url, f) =
new HttpAgent(url, f)
httpServer.fs :
module httpServer
open Server.Core
let execute = fun ( server : HttpAgent) -> async {
while true do
let! ctx = server.Receive()
ctx.Response.Reply(ctx.Request.InputString) }
This code runs well in a console project (ie: I can access it with a browser, it does find it) :
[<EntryPoint>]
let main argv =
let siteRoot = #"D:\Projects\flaming-octo-spice\src\Site"
let url = "http://localhost:8082/"
let server = HttpAgent.Start(url, httpServer.execute)
printfn "%A" argv
let s = Console.ReadLine()
// Stop the HTTP server and release the port 8082
server.Stop()
0 // return an integer exit code
whereas in my test, I cannot access the server. I have even put some breakpoint in order to check with my browser if the server was up and running , but chrome tells me no host exists with ths url.
namespace UnitTestProject1
open System
open Microsoft.VisualStudio.TestTools.UnitTesting
open Server.Core
open System.Net.Http
[<TestClass>]
type HttpServerTests() =
[<TestMethod>]
member x.Should_start_a_web_site_with_host_address () =
let host = "http://localhost:8082/"
let server = HttpAgent.Start(host, httpServer.execute)
let url = "http://localhost:8082/test/url"
let client = new HttpClient()
let response = client.GetAsync(url)
Assert.IsTrue(response.Result.IsSuccessStatusCode )
Thanks for any enlightment...
You're starting server at port 8092, but client tries to access it at 8082.