Skip to content

Commit 206e7ed

Browse files
committed
Upgrade to .NET 10 + add robots blocking
1 parent fcffb22 commit 206e7ed

10 files changed

Lines changed: 255 additions & 28 deletions

File tree

.github/workflows/build-container.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
if: ${{ github.event.workflow_run.conclusion == 'success' }}
2424
steps:
2525
- name: Checkout code
26-
uses: actions/checkout@v3
26+
uses: actions/checkout@v5
2727

2828
- name: Set up environment variables
2929
run: |
@@ -61,7 +61,7 @@ jobs:
6161
if: steps.check_client.outputs.client_exists == 'true'
6262
uses: actions/setup-node@v3
6363
with:
64-
node-version: 22
64+
node-version: 24
6565

6666
- name: Install npm dependencies
6767
if: steps.check_client.outputs.client_exists == 'true'
@@ -88,9 +88,9 @@ jobs:
8888
password: ${{ env.KAMAL_REGISTRY_PASSWORD }}
8989

9090
- name: Setup .NET
91-
uses: actions/setup-dotnet@v3
91+
uses: actions/setup-dotnet@v5
9292
with:
93-
dotnet-version: '8.0'
93+
dotnet-version: 10.0.x
9494

9595
- name: Build and push Docker image
9696
run: |

.github/workflows/build.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ on:
88

99
jobs:
1010
build:
11-
runs-on: ubuntu-22.04
11+
runs-on: ubuntu-latest
1212
steps:
1313
- name: checkout
14-
uses: actions/checkout@v3
14+
uses: actions/checkout@v5
1515

1616
- name: Setup dotnet
17-
uses: actions/setup-dotnet@v3
17+
uses: actions/setup-dotnet@v5
1818
with:
19-
dotnet-version: '8.0'
19+
dotnet-version: 10.0.x
2020

2121
- name: build
2222
run: dotnet build

ExampleDataApis.ServiceInterface/ExampleDataApis.ServiceInterface.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>net8.0</TargetFramework>
4+
<TargetFramework>net10.0</TargetFramework>
55
</PropertyGroup>
66

77
<ItemGroup>
8-
<PackageReference Include="ServiceStack" Version="8.*" />
8+
<PackageReference Include="ServiceStack" Version="10.*" />
99
</ItemGroup>
1010

1111
<ItemGroup>

ExampleDataApis.ServiceModel/ExampleDataApis.ServiceModel.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>net8.0</TargetFramework>
4+
<TargetFramework>net10.0</TargetFramework>
55
</PropertyGroup>
66

77
<ItemGroup>
8-
<PackageReference Include="ServiceStack.Interfaces" Version="8.*" />
8+
<PackageReference Include="ServiceStack.Interfaces" Version="10.*" />
99
</ItemGroup>
1010

1111
<ItemGroup>

ExampleDataApis.Tests/ExampleDataApis.Tests.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>net8.0</TargetFramework>
4+
<TargetFramework>net10.0</TargetFramework>
55
<DebugType>portable</DebugType>
66
<OutputType>Library</OutputType>
77
</PropertyGroup>
@@ -13,8 +13,8 @@
1313
<PackageReference Include="NUnit" Version="3.13.*" />
1414
<PackageReference Include="NUnit3TestAdapter" Version="4.1.*" />
1515
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.*" />
16-
<PackageReference Include="ServiceStack" Version="8.*" />
17-
<PackageReference Include="ServiceStack.Kestrel" Version="8.*" />
16+
<PackageReference Include="ServiceStack" Version="10.*" />
17+
<PackageReference Include="ServiceStack.Kestrel" Version="10.*" />
1818
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />
1919
</ItemGroup>
2020

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
using Microsoft.Extensions.Options;
2+
3+
namespace ExampleDataApis;
4+
5+
/// <summary>
6+
/// Options for configuring the UserAgentBlockingMiddleware
7+
/// </summary>
8+
public class UserAgentBlockingOptions
9+
{
10+
/// <summary>
11+
/// List of user agents to block (supports exact matches or substring matches)
12+
/// </summary>
13+
public List<string> BlockedUserAgents { get; set; } = [];
14+
15+
public List<string> BlockedIps { get; set; } = [];
16+
17+
/// <summary>
18+
/// HTTP status code to return when a user agent is blocked (defaults to 403 Forbidden)
19+
/// </summary>
20+
public int BlockedStatusCode { get; set; } = StatusCodes.Status403Forbidden;
21+
22+
/// <summary>
23+
/// Optional message to return in the response body when a user agent is blocked
24+
/// </summary>
25+
public string BlockedMessage { get; set; } = "Access denied based on your user agent";
26+
27+
/// <summary>
28+
/// If true, will perform case-insensitive matching (defaults to true)
29+
/// </summary>
30+
public bool IgnoreCase { get; set; } = true;
31+
32+
/// <summary>
33+
/// If true, blocked requests will be logged (defaults to true)
34+
/// </summary>
35+
public bool LogBlockedRequests { get; set; } = true;
36+
}
37+
38+
/// <summary>
39+
/// Middleware that blocks requests from specific user agents
40+
/// </summary>
41+
public class UserAgentBlockingMiddleware(
42+
RequestDelegate next,
43+
IOptions<UserAgentBlockingOptions> options,
44+
ILogger<UserAgentBlockingMiddleware> logger)
45+
{
46+
UserAgentBlockingOptions Options => options?.Value ?? throw new ArgumentNullException(nameof(options));
47+
48+
public async Task InvokeAsync(HttpContext context)
49+
{
50+
if (context == null)
51+
{
52+
throw new ArgumentNullException(nameof(context));
53+
}
54+
55+
// Get the client IP address
56+
var remoteIp = context.Connection.RemoteIpAddress?.ToString();
57+
58+
// Check if the IP should be blocked
59+
if (ShouldBlockIp(remoteIp))
60+
{
61+
if (Options.LogBlockedRequests)
62+
{
63+
logger.LogInformation(
64+
"Request blocked from IP: {IPAddress}, Path: {Path}",
65+
remoteIp,
66+
context.Request.Path);
67+
}
68+
69+
context.Response.StatusCode = Options.BlockedStatusCode;
70+
context.Response.ContentType = "text/plain";
71+
await context.Response.WriteAsync(Options.BlockedMessage);
72+
return;
73+
}
74+
75+
// Get the User-Agent header
76+
string userAgent = context.Request.Headers["User-Agent"].ToString();
77+
78+
// Check if the user agent should be blocked
79+
if (ShouldBlockUserAgent(userAgent))
80+
{
81+
// Log the blocked request if enabled
82+
if (Options.LogBlockedRequests)
83+
{
84+
logger.LogInformation(
85+
"Request blocked from user agent: {UserAgent}, IP: {IPAddress}, Path: {Path}",
86+
userAgent,
87+
remoteIp,
88+
context.Request.Path);
89+
}
90+
91+
// Set the response status code
92+
context.Response.StatusCode = Options.BlockedStatusCode;
93+
context.Response.ContentType = "text/plain";
94+
95+
// Write the blocked message to the response
96+
await context.Response.WriteAsync(Options.BlockedMessage);
97+
return;
98+
}
99+
100+
// If not blocked, continue to the next middleware
101+
await next(context);
102+
}
103+
104+
private bool ShouldBlockUserAgent(string userAgent)
105+
{
106+
if (string.IsNullOrEmpty(userAgent))
107+
{
108+
// You might want to block requests with no user agent
109+
// Return true here if you want to block empty user agents
110+
return false;
111+
}
112+
113+
foreach (var blockedAgent in Options.BlockedUserAgents)
114+
{
115+
var comparison = Options.IgnoreCase
116+
? StringComparison.OrdinalIgnoreCase
117+
: StringComparison.Ordinal;
118+
119+
if (userAgent.Contains(blockedAgent, comparison))
120+
return true;
121+
if (blockedAgent.Contains(' ') && userAgent.Contains(blockedAgent.Replace(" ",""), comparison))
122+
return true;
123+
}
124+
125+
return false;
126+
}
127+
128+
private bool ShouldBlockIp(string? ipAddress)
129+
{
130+
if (string.IsNullOrEmpty(ipAddress))
131+
return false;
132+
133+
foreach (var blockedIp in Options.BlockedIps)
134+
{
135+
// Support partial IP matching (e.g., "114.119" blocks all IPs starting with "114.119")
136+
if (ipAddress.StartsWith(blockedIp, StringComparison.Ordinal))
137+
return true;
138+
}
139+
140+
return false;
141+
}
142+
}
143+
144+
/// <summary>
145+
/// Extension methods for registering the UserAgentBlockingMiddleware
146+
/// </summary>
147+
public static class UserAgentBlockingMiddlewareExtensions
148+
{
149+
/// <summary>
150+
/// Adds the UserAgentBlockingMiddleware to the application pipeline
151+
/// </summary>
152+
public static IApplicationBuilder UseUserAgentBlocking(
153+
this IApplicationBuilder builder)
154+
{
155+
return builder.UseMiddleware<UserAgentBlockingMiddleware>();
156+
}
157+
158+
/// <summary>
159+
/// Adds the UserAgentBlockingMiddleware to the application pipeline with custom options
160+
/// </summary>
161+
public static IApplicationBuilder UseUserAgentBlocking(
162+
this IApplicationBuilder builder,
163+
Action<UserAgentBlockingOptions> configureOptions)
164+
{
165+
// Create a new options instance
166+
var options = new UserAgentBlockingOptions();
167+
168+
// Apply the configuration
169+
configureOptions(options);
170+
171+
// Use the middleware with the configured options
172+
return builder.UseMiddleware<UserAgentBlockingMiddleware>(Options.Create(options));
173+
}
174+
}

ExampleDataApis/ExampleDataApis.csproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk.Web">
22

33
<PropertyGroup>
4-
<TargetFramework>net8.0</TargetFramework>
4+
<TargetFramework>net10.0</TargetFramework>
55
<Nullable>enable</Nullable>
66
<ImplicitUsings>enable</ImplicitUsings>
77
<PublishProfile>DefaultContainer</PublishProfile>
@@ -22,9 +22,9 @@
2222
</ItemGroup>
2323

2424
<ItemGroup>
25-
<PackageReference Include="ServiceStack" Version="8.*" />
26-
<PackageReference Include="ServiceStack.OrmLite.Sqlite.Data" Version="8.*" />
27-
<PackageReference Include="ServiceStack.Server" Version="8.*" />
25+
<PackageReference Include="ServiceStack" Version="10.*" />
26+
<PackageReference Include="ServiceStack.OrmLite.Sqlite.Data" Version="10.*" />
27+
<PackageReference Include="ServiceStack.Server" Version="10.*" />
2828
</ItemGroup>
2929

3030
<ItemGroup>

ExampleDataApis/Program.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,35 @@
11
var builder = WebApplication.CreateBuilder(args);
2+
var services = builder.Services;
3+
4+
services.Configure<UserAgentBlockingOptions>(options =>
5+
{
6+
// Add user agents to block
7+
options.BlockedUserAgents.AddRange([
8+
"bytespider",
9+
"gptbot",
10+
"gptbot",
11+
"claudebot",
12+
"amazonbot",
13+
"imagesiftbot",
14+
"semrushbot",
15+
"dotbot",
16+
"semrushbot",
17+
"dataforseobot",
18+
"WhatsApp Bot",
19+
// "HeadlessChrome",
20+
"PetalBot",
21+
]);
22+
23+
options.BlockedIps.AddRange([
24+
"114.119"
25+
]);
26+
27+
// Optional: Customize the response status code
28+
// options.BlockedStatusCode = StatusCodes.Status429TooManyRequests;
29+
30+
// Optional: Customize the blocked message
31+
options.BlockedMessage = "This bot is not allowed to access our website";
32+
});
233

334
var app = builder.Build();
435

ExampleDataApis/wwwroot/index.html

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<html>
22
<head>
3-
<title>Example Data Apis</title>
3+
<title>My App</title>
44
<style>
55
body { padding: 1em 1em 5em 1em; }
66
body, input[type=text] { font: 20px/28px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif }
@@ -12,7 +12,6 @@
1212
h2, h3, strong { font-weight:500 }
1313
</style>
1414
<link rel="stylesheet" href="https://unpkg.com/@highlightjs/cdn-assets@11.7.0/styles/atom-one-dark.min.css">
15-
<script async src="https://ga.jspm.io/npm:es-module-shims@1.6.3/dist/es-module-shims.js"></script><!--safari polyfill-->
1615
<script type="importmap">
1716
{
1817
"imports": {
@@ -28,10 +27,10 @@ <h2><a href="/ui/Hello">Hello</a> API</h2>
2827
<div id="result"></div>
2928

3029
<script type="module">
31-
import { JsonApiClient, $1, on } from '@servicestack/client'
30+
import { JsonServiceClient, $1, on } from '@servicestack/client'
3231
import { Hello } from '/types/mjs'
3332

34-
const client = JsonApiClient.create()
33+
const client = new JsonServiceClient()
3534
on('#txtName', {
3635
/** @param {Event} el */
3736
async keyup(el) {
@@ -51,8 +50,8 @@ <h2><a href="/ui/Hello">Hello</a> API</h2>
5150

5251
- [Call API](/ui/Hello)
5352
- [View API Details](/ui/Hello?tab=details)
54-
- [Browse API source code in different langauges](/ui/Hello?tab=code)
55-
53+
- [Browse API source code in different languages](/ui/Hello?tab=code)
54+
5655
### Using JsonServiceClient in Web Pages
5756

5857
Easiest way to call APIs is to use [@servicestack/client](https://docs.servicestack.net/javascript-client) with
@@ -81,10 +80,10 @@ <h2><a href="/ui/Hello">Hello</a> API</h2>
8180

8281
```html
8382
&lt;script type="module"&gt;
84-
import { JsonApiClient, $1, on } from '@servicestack/client'
83+
import { JsonServiceClient, $1, on } from '@servicestack/client'
8584
import { Hello } from '/types/mjs'
8685

87-
const client = JsonApiClient.create()
86+
const client = new JsonServiceClient()
8887
on('#txtName', {
8988
async keyup(el) {
9089
const api = await client.api(new Hello({ name:el.target.value }))
@@ -99,7 +98,7 @@ <h2><a href="/ui/Hello">Hello</a> API</h2>
9998
For better IDE intelli-sense during development, save the annotated Typed DTOs to disk with the [x dotnet tool](https://docs.servicestack.net/dotnet-tool):
10099

101100
```bash
102-
$ x mjs
101+
npx get-dtos mjs
103102
```
104103

105104
Then reference it instead to enable IDE static analysis when calling Typed APIs from JavaScript:

0 commit comments

Comments
 (0)