Skip to content

Commit 8e403c4

Browse files
committed
Merge branch 'master' into feat/deps
2 parents 06b1c8d + 799ee50 commit 8e403c4

File tree

11 files changed

+746
-80
lines changed

11 files changed

+746
-80
lines changed

AGENTS.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
## Build, Test & Lint Commands
2+
3+
- **Build**: `dotnet fake build -t Build` (Release configuration)
4+
- **Format Check**: `dotnet fake build -t CheckFormat` (validates Fantomas formatting)
5+
- **Format**: `dotnet fake build -t Format` (applies Fantomas formatting)
6+
- **All Tests**: `dotnet fake build -t RunTests` (builds + starts test server + runs all tests)
7+
- **Unit Tests Only**: `dotnet build && dotnet tests/SwaggerProvider.Tests/bin/Release/net9.0/SwaggerProvider.Tests.dll`
8+
- **Provider Tests (Integration)**:
9+
1. Build test server: `dotnet build tests/Swashbuckle.WebApi.Server/Swashbuckle.WebApi.Server.fsproj -c Release`
10+
2. Start server in background: `dotnet tests/Swashbuckle.WebApi.Server/bin/Release/net9.0/Swashbuckle.WebApi.Server.dll`
11+
3. Build tests: `dotnet build SwaggerProvider.TestsAndDocs.sln -c Release`
12+
4. Run tests: `dotnet tests/SwaggerProvider.ProviderTests/bin/Release/net9.0/SwaggerProvider.ProviderTests.dll`
13+
- **Single Test**: Run via xunit runner: `dotnet [assembly] [filter]`
14+
15+
## Code Style Guidelines
16+
17+
**Language**: F# (net9.0 target framework)
18+
19+
**Imports & Namespaces**:
20+
21+
- `namespace [Module]` at file start; no `open` statements at module level
22+
- Use `module [Name]` for nested modules
23+
- Open dependencies after namespace declaration (e.g., `open Xunit`, `open FsUnitTyped`)
24+
- Fully qualify internal modules: `SwaggerProvider.Internal.v2.Parser`, `SwaggerProvider.Internal.v3.Compilers`
25+
26+
**Formatting** (via Fantomas, EditorConfig enforced):
27+
28+
- 4-space indents, max 150 char line length
29+
- `fsharp_max_function_binding_width=10`, `fsharp_max_infix_operator_expression=70`
30+
- No space before parameter/lowercase invocation
31+
- Multiline block brackets on same column, Stroustrup style enabled
32+
- Bar before discriminated union declarations, max 3 blank lines
33+
34+
**Naming Conventions**:
35+
36+
- PascalCase for classes, types, modules, public members
37+
- camelCase for local/private bindings, parameters
38+
- Suffix test functions with `Tests` or use attributes like `[<Theory>]`, `[<Fact>]`
39+
40+
**Type Annotations**:
41+
42+
- Explicit return types for public functions (recommended)
43+
- Use type inference for local bindings when obvious
44+
- Generic type parameters: `'a`, `'b` (single quote prefix)
45+
46+
**Error Handling**:
47+
48+
- Use `Result<'T, 'Error>` or `Option<'T>` for fallible operations
49+
- `failwith` or `failwithf` for errors in type providers and compilers
50+
- Task-based async for I/O: `task { }` expressions in tests
51+
- Match failures with `| _ -> ...` or pattern guards with `when`
52+
53+
**File Organization**:
54+
55+
- Tests use Xunit attributes: `[<Theory>]`, `[<Fact>]`, `[<MemberData>]`
56+
- Design-time providers in `src/SwaggerProvider.DesignTime/`, runtime in `src/SwaggerProvider.Runtime/`
57+
- Test schemas organized by OpenAPI version: `tests/.../Schemas/{v2,v3}/`
58+
59+
## Key Patterns
60+
61+
- Type Providers use `ProvidedApiClientBase` and compiler pipeline (DefinitionCompiler, OperationCompiler)
62+
- SSRF protection enabled by default; disable with `SsrfProtection=false` static parameter
63+
- Target net9.0; use implicit async/await (task expressions)

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ This SwaggerProvider can be used to access RESTful API generated using [Swagger.
66

77
Documentation: <http://fsprojects.github.io/SwaggerProvider/>
88

9+
**Security:** SSRF protection is enabled by default. For local development, use static parameter `SsrfProtection=false`.
10+
911
## Swagger RESTful API Documentation Specification
1012

1113
Swagger is available for ASP.NET WebAPI APIs with [Swashbuckle](https://github.com/domaindrivendev/Swashbuckle).

docs/OpenApiClientProvider.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,26 @@ let client = PetStore.Client()
2121
| `IgnoreControllerPrefix` | Do not parse `operationsId` as `<controllerName>_<methodName>` and generate one client class for all operations. Default value `true`. |
2222
| `PreferNullable` | Provide `Nullable<_>` for not required properties, instead of `Option<_>`. Defaults value `false`. |
2323
| `PreferAsync` | Generate async actions of type `Async<'T>` instead of `Task<'T>`. Defaults value `false`. |
24+
| `SsrfProtection` | Enable SSRF protection (blocks HTTP and localhost). Set to `false` for development/testing. Default value `true`. |
2425

2526
More configuration scenarios are described in [Customization section](/Customization)
2627

28+
## Security (SSRF Protection)
29+
30+
By default, SwaggerProvider blocks HTTP URLs and localhost/private IP addresses to prevent [SSRF attacks](https://owasp.org/www-community/attacks/Server_Side_Request_Forgery).
31+
32+
For **development and testing** with local servers, disable SSRF protection:
33+
34+
```fsharp
35+
// Development: Allow HTTP and localhost
36+
type LocalApi = OpenApiClientProvider<"http://localhost:5000/swagger.json", SsrfProtection=false>
37+
38+
// Production: HTTPS with SSRF protection (default)
39+
type ProdApi = OpenApiClientProvider<"https://api.example.com/swagger.json">
40+
```
41+
42+
**Warning:** Never set `SsrfProtection=false` in production code.
43+
2744
## Sample
2845

2946
Sample uses [TaskBuilder.fs](https://github.com/rspeele/TaskBuilder.fs) (F# computation expression builder for System.Threading.Tasks) that will become part of [Fsharp.Core.dll] one day [[WIP, RFC FS-1072] task support](https://github.com/dotnet/fsharp/pull/6811).

docs/SwaggerClientProvider.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,26 @@ When you use TP you can specify the following parameters
2828
| `IgnoreControllerPrefix` | Do not parse `operationsId` as `<controllerName>_<methodName>` and generate one client class for all operations. Default value `true`. |
2929
| `PreferNullable` | Provide `Nullable<_>` for not required properties, instead of `Option<_>`. Defaults value `false`. |
3030
| `PreferAsync` | Generate async actions of type `Async<'T>` instead of `Task<'T>`. Defaults value `false`. |
31+
| `SsrfProtection` | Enable SSRF protection (blocks HTTP and localhost). Set to `false` for development/testing. Default value `true`. |
3132

3233
More configuration scenarios are described in [Customization section](/Customization)
3334

35+
## Security (SSRF Protection)
36+
37+
By default, SwaggerProvider blocks HTTP URLs and localhost/private IP addresses to prevent [SSRF attacks](https://owasp.org/www-community/attacks/Server_Side_Request_Forgery).
38+
39+
For **development and testing** with local servers, disable SSRF protection:
40+
41+
```fsharp
42+
// Development: Allow HTTP and localhost
43+
type LocalApi = SwaggerClientProvider<"http://localhost:5000/swagger.json", SsrfProtection=false>
44+
45+
// Production: HTTPS with SSRF protection (default)
46+
type ProdApi = SwaggerClientProvider<"https://api.example.com/swagger.json">
47+
```
48+
49+
**Warning:** Never set `SsrfProtection=false` in production code.
50+
3451
## Sample
3552

3653
The usage is very similar to [OpenApiClientProvider](/OpenApiClientProvider#sample)

src/SwaggerProvider.DesignTime/Provider.OpenApiClient.fs

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ open Swagger
99
open SwaggerProvider.Internal
1010
open SwaggerProvider.Internal.v3.Compilers
1111

12-
module Cache =
12+
module OpenApiCache =
1313
let providedTypes = Caching.createInMemoryCache(TimeSpan.FromSeconds 30.0)
1414

1515
/// The Open API Provider.
@@ -37,36 +37,39 @@ type public OpenApiClientTypeProvider(cfg: TypeProviderConfig) as this =
3737
ProvidedStaticParameter("IgnoreOperationId", typeof<bool>, false)
3838
ProvidedStaticParameter("IgnoreControllerPrefix", typeof<bool>, true)
3939
ProvidedStaticParameter("PreferNullable", typeof<bool>, false)
40-
ProvidedStaticParameter("PreferAsync", typeof<bool>, false) ]
40+
ProvidedStaticParameter("PreferAsync", typeof<bool>, false)
41+
ProvidedStaticParameter("SsrfProtection", typeof<bool>, true) ]
4142

4243
t.AddXmlDoc
4344
"""<summary>Statically typed OpenAPI provider.</summary>
4445
<param name='Schema'>Url or Path to OpenAPI schema file.</param>
4546
<param name='IgnoreOperationId'>Do not use `operationsId` and generate method names using `path` only. Default value `false`.</param>
4647
<param name='IgnoreControllerPrefix'>Do not parse `operationsId` as `<controllerName>_<methodName>` and generate one client class for all operations. Default value `true`.</param>
4748
<param name='PreferNullable'>Provide `Nullable<_>` for not required properties, instead of `Option<_>`. Defaults value `false`.</param>
48-
<param name='PreferAsync'>Generate async actions of type `Async<'T>` instead of `Task<'T>`. Defaults value `false`.</param>"""
49+
<param name='PreferAsync'>Generate async actions of type `Async<'T>` instead of `Task<'T>`. Defaults value `false`.</param>
50+
<param name='SsrfProtection'>Enable SSRF protection (blocks HTTP and localhost). Set to false for development/testing. Default value `true`.</param>"""
4951

5052
t.DefineStaticParameters(
5153
staticParams,
5254
fun typeName args ->
53-
let schemaPath =
54-
let schemaPathRaw = unbox<string> args.[0]
55-
SchemaReader.getAbsolutePath cfg.ResolutionFolder schemaPathRaw
56-
55+
let schemaPathRaw = unbox<string> args.[0]
5756
let ignoreOperationId = unbox<bool> args.[1]
5857
let ignoreControllerPrefix = unbox<bool> args.[2]
5958
let preferNullable = unbox<bool> args.[3]
6059
let preferAsync = unbox<bool> args.[4]
60+
let ssrfProtection = unbox<bool> args.[5]
6161

6262
let cacheKey =
63-
(schemaPath, ignoreOperationId, ignoreControllerPrefix, preferNullable, preferAsync)
63+
(schemaPathRaw, ignoreOperationId, ignoreControllerPrefix, preferNullable, preferAsync, ssrfProtection)
6464
|> sprintf "%A"
6565

66-
6766
let addCache() =
6867
lazy
69-
let schemaData = SchemaReader.readSchemaPath "" schemaPath |> Async.RunSynchronously
68+
let schemaData =
69+
SchemaReader.readSchemaPath (not ssrfProtection) "" cfg.ResolutionFolder schemaPathRaw
70+
|> Async.RunSynchronously
71+
72+
let openApiReader = Microsoft.OpenApi.Readers.OpenApiStringReader()
7073

7174
let settings = OpenApiReaderSettings()
7275
settings.AddYamlReader()
@@ -96,18 +99,18 @@ type public OpenApiClientTypeProvider(cfg: TypeProviderConfig) as this =
9699
let ty =
97100
ProvidedTypeDefinition(tempAsm, ns, typeName, Some typeof<obj>, isErased = false, hideObjectMethods = true)
98101

99-
ty.AddXmlDoc("OpenAPI Provider for " + schemaPath)
102+
ty.AddXmlDoc("OpenAPI Provider for " + schemaPathRaw)
100103
ty.AddMembers tys
101104
tempAsm.AddTypes [ ty ]
102105

103106
ty
104107

105108
try
106-
Cache.providedTypes.GetOrAdd(cacheKey, addCache).Value
109+
OpenApiCache.providedTypes.GetOrAdd(cacheKey, addCache).Value
107110
with _ ->
108-
Cache.providedTypes.Remove(cacheKey) |> ignore
111+
OpenApiCache.providedTypes.Remove(cacheKey) |> ignore
109112

110-
Cache.providedTypes.GetOrAdd(cacheKey, addCache).Value
113+
OpenApiCache.providedTypes.GetOrAdd(cacheKey, addCache).Value
111114
)
112115

113116
t

src/SwaggerProvider.DesignTime/Provider.SwaggerClient.fs

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ open SwaggerProvider.Internal
99
open SwaggerProvider.Internal.v2.Parser
1010
open SwaggerProvider.Internal.v2.Compilers
1111

12+
module SwaggerCache =
13+
let providedTypes = Caching.createInMemoryCache(TimeSpan.FromSeconds 30.0)
14+
1215
/// The Swagger Type Provider.
1316
[<TypeProvider; Obsolete("Use OpenApiClientTypeProvider when possible, it supports v2 & v3 schema formats.")>]
1417
type public SwaggerTypeProvider(cfg: TypeProviderConfig) as this =
@@ -35,7 +38,8 @@ type public SwaggerTypeProvider(cfg: TypeProviderConfig) as this =
3538
ProvidedStaticParameter("IgnoreOperationId", typeof<bool>, false)
3639
ProvidedStaticParameter("IgnoreControllerPrefix", typeof<bool>, true)
3740
ProvidedStaticParameter("PreferNullable", typeof<bool>, false)
38-
ProvidedStaticParameter("PreferAsync", typeof<bool>, false) ]
41+
ProvidedStaticParameter("PreferAsync", typeof<bool>, false)
42+
ProvidedStaticParameter("SsrfProtection", typeof<bool>, true) ]
3943

4044
t.AddXmlDoc
4145
"""<summary>Statically typed Swagger provider.</summary>
@@ -44,29 +48,28 @@ type public SwaggerTypeProvider(cfg: TypeProviderConfig) as this =
4448
<param name='IgnoreOperationId'>Do not use `operationsId` and generate method names using `path` only. Default value `false`.</param>
4549
<param name='IgnoreControllerPrefix'>Do not parse `operationsId` as `<controllerName>_<methodName>` and generate one client class for all operations. Default value `true`.</param>
4650
<param name='PreferNullable'>Provide `Nullable<_>` for not required properties, instead of `Option<_>`. Defaults value `false`.</param>
47-
<param name='PreferAsync'>Generate async actions of type `Async<'T>` instead of `Task<'T>`. Defaults value `false`.</param>"""
51+
<param name='PreferAsync'>Generate async actions of type `Async<'T>` instead of `Task<'T>`. Defaults value `false`.</param>
52+
<param name='SsrfProtection'>Enable SSRF protection (blocks HTTP and localhost). Set to false for development/testing. Default value `true`.</param>"""
4853

4954
t.DefineStaticParameters(
5055
staticParams,
5156
fun typeName args ->
52-
let schemaPath =
53-
let schemaPathRaw = unbox<string> args.[0]
54-
SchemaReader.getAbsolutePath cfg.ResolutionFolder schemaPathRaw
55-
57+
let schemaPathRaw = unbox<string> args.[0]
5658
let headersStr = unbox<string> args.[1]
5759
let ignoreOperationId = unbox<bool> args.[2]
5860
let ignoreControllerPrefix = unbox<bool> args.[3]
5961
let preferNullable = unbox<bool> args.[4]
6062
let preferAsync = unbox<bool> args.[5]
63+
let ssrfProtection = unbox<bool> args.[6]
6164

6265
let cacheKey =
63-
(schemaPath, headersStr, ignoreOperationId, ignoreControllerPrefix, preferNullable, preferAsync)
66+
(schemaPathRaw, headersStr, ignoreOperationId, ignoreControllerPrefix, preferNullable, preferAsync, ssrfProtection)
6467
|> sprintf "%A"
6568

6669
let addCache() =
6770
lazy
6871
let schemaData =
69-
SchemaReader.readSchemaPath headersStr schemaPath
72+
SchemaReader.readSchemaPath (not ssrfProtection) headersStr cfg.ResolutionFolder schemaPathRaw
7073
|> Async.RunSynchronously
7174

7275
let schema = SwaggerParser.parseSchema schemaData
@@ -84,13 +87,13 @@ type public SwaggerTypeProvider(cfg: TypeProviderConfig) as this =
8487
let ty =
8588
ProvidedTypeDefinition(tempAsm, ns, typeName, Some typeof<obj>, isErased = false, hideObjectMethods = true)
8689

87-
ty.AddXmlDoc("Swagger Provider for " + schemaPath)
90+
ty.AddXmlDoc("Swagger Provider for " + schemaPathRaw)
8891
ty.AddMembers tys
8992
tempAsm.AddTypes [ ty ]
9093

9194
ty
9295

93-
Cache.providedTypes.GetOrAdd(cacheKey, addCache).Value
96+
SwaggerCache.providedTypes.GetOrAdd(cacheKey, addCache).Value
9497
)
9598

9699
t

0 commit comments

Comments
 (0)