Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ curl https://api.ipsimple.org/api/ip/v6

# Get all detected addresses
curl https://api.ipsimple.org/api/ip/all
# Get geolocation for client IP
curl https://api.ipsimple.org/api/ip/geolocation
# Get geolocation for specific IP
curl https://api.ipsimple.org/api/ip/geolocation/203.0.113.1
# Submit bulk IP processing job
curl -X POST https://api.ipsimple.org/api/ip/bulk -d '{"ips": ["203.0.113.1"]}'
```

### Response Format
Expand Down Expand Up @@ -140,8 +146,13 @@ Once running, you can test the following endpoints:

- `GET /api/ip` - Get all IP address information in JSON format
- `GET /api/ip/v4` - Get IPv4 address only
- `GET /api/ip/v6` - Get IPv6 address only
- `GET /api/ip/v6` - Get IPv6 address only
- `GET /api/ip/all` - Get all detected IP addresses
- `GET /api/ip/geolocation` - Geolocation for client IP
- `GET /api/ip/geolocation/{ip}` - Geolocation for a specific IP
- `POST /api/ip/bulk` - Submit bulk IP processing job
- `GET /api/ip/bulk/{jobId}` - Check bulk job status
- `GET /api/ip/bulk/{jobId}/results` - Download bulk job results

### Testing the API

Expand Down
58 changes: 58 additions & 0 deletions src/IpSimple.PublicIp.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ public static WebApplication CreateWebApp(string[] args)
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddSingleton<IIpAddressService, IpAddressService>();
builder.Services.AddSingleton<IGeolocationService, GeolocationService>();
builder.Services.AddSingleton<IBulkIpProcessingService, BulkIpProcessingService>();

var app = builder.Build();

Expand All @@ -28,6 +30,8 @@ public static WebApplication CreateWebApp(string[] args)
}

var ipAddressService = app.Services.GetRequiredService<IIpAddressService>();
var geolocationService = app.Services.GetRequiredService<IGeolocationService>();
var bulkService = app.Services.GetRequiredService<IBulkIpProcessingService>();

app.MapGet("/", ipAddressService.GetClientIpv4)
.WithName("Default")
Expand Down Expand Up @@ -56,6 +60,15 @@ public static WebApplication CreateWebApp(string[] args)
return operation;
});

app.MapGet("/all", ipAddressService.GetClientIps)
.WithName("GetDualStackIps")
.WithOpenApi(operation =>
{
operation.Summary = "Dual stack IP addresses";
operation.Description = "Returns both IPv4 and IPv6 addresses if available.";
return operation;
});

app.MapGet("/ipv6", ipAddressService.GetClientIpv6)
.WithName("GetPublicIPv6")
.WithOpenApi(operation =>
Expand All @@ -74,6 +87,51 @@ public static WebApplication CreateWebApp(string[] args)
return operation;
});

app.MapGet("/geolocation", geolocationService.GetGeolocationForClient)
.WithName("GetClientGeolocation")
.WithOpenApi(operation =>
{
operation.Summary = "Geolocation for client IP";
operation.Description = "Returns geolocation metadata for the calling client.";
return operation;
});

app.MapGet("/geolocation/{ipAddress}", geolocationService.GetGeolocationForIp)
.WithName("GetIpGeolocation")
.WithOpenApi(operation =>
{
operation.Summary = "Geolocation lookup";
operation.Description = "Provides geolocation metadata for a specific IP address.";
return operation;
});

app.MapPost("/bulk", bulkService.SubmitBulkJob)
.WithName("SubmitBulkIpJob")
.WithOpenApi(operation =>
{
operation.Summary = "Submit bulk IP processing";
operation.Description = "Accepts a list of IP addresses for asynchronous processing.";
return operation;
});

app.MapGet("/bulk/{jobId}", bulkService.GetJobStatus)
.WithName("GetBulkJobStatus")
.WithOpenApi(operation =>
{
operation.Summary = "Bulk job status";
operation.Description = "Retrieves processing status for a submitted bulk IP job.";
return operation;
});

app.MapGet("/bulk/{jobId}/results", bulkService.GetJobResults)
.WithName("GetBulkJobResults")
.WithOpenApi(operation =>
{
operation.Summary = "Bulk job results";
operation.Description = "Gets processed results for a completed bulk IP job.";
return operation;
});

app.MapGet("/version", () =>
{
var version = Environment.GetEnvironmentVariable("APP_VERSION") ?? "unknown";
Expand Down
28 changes: 28 additions & 0 deletions src/IpSimple.PublicIp.Api/Services/BulkIpProcessingService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using IpSimple.Domain.Settings;

namespace IpSimple.PublicIp.Api.Services;

public class BulkIpProcessingService : IBulkIpProcessingService
{
public IResult SubmitBulkJob(HttpContext httpContext)
{
// TODO: Parse incoming IP list from body or uploaded file.
// Queue background job for asynchronous processing (see issue #15).
// Return job identifier for client to poll.
return Results.Json(new { message = "Bulk IP processing not yet implemented" }, JsonSerializerSettings.DefaultJsonSerializer);
}

public IResult GetJobStatus(string jobId)
{
// TODO: Retrieve job status from persistent store or in-memory cache.
// Provide percentage complete and any available metadata.
return Results.Json(new { jobId, status = "pending" }, JsonSerializerSettings.DefaultJsonSerializer);
}

public IResult GetJobResults(string jobId)
{
// TODO: Return aggregated results for completed job.
// Support download of large result sets and handle pagination.
return Results.Json(new { jobId, results = Array.Empty<object>() }, JsonSerializerSettings.DefaultJsonSerializer);
}
}
23 changes: 23 additions & 0 deletions src/IpSimple.PublicIp.Api/Services/GeolocationService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using IpSimple.Domain.Settings;
using IpSimple.Extensions;

namespace IpSimple.PublicIp.Api.Services;

public class GeolocationService : IGeolocationService
{
public IResult GetGeolocationForClient(HttpContext httpContext)
{
// TODO: Extract client IP using existing helpers
// and call external geolocation provider (e.g. MaxMind).
// Return structured geolocation data similar to issue #14 spec.
return Results.Json(new { message = "Geolocation lookup not yet implemented" }, JsonSerializerSettings.DefaultJsonSerializer);
}

public IResult GetGeolocationForIp(string ipAddress)
{
// TODO: Validate the provided IP address and query
// the geolocation provider for metadata. Cache results
// and handle provider errors appropriately.
return Results.Json(new { message = "Geolocation lookup not yet implemented", ipAddress }, JsonSerializerSettings.DefaultJsonSerializer);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace IpSimple.PublicIp.Api.Services;

public interface IBulkIpProcessingService
{
IResult SubmitBulkJob(HttpContext httpContext);
IResult GetJobStatus(string jobId);
IResult GetJobResults(string jobId);
}
7 changes: 7 additions & 0 deletions src/IpSimple.PublicIp.Api/Services/IGeolocationService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace IpSimple.PublicIp.Api.Services;

public interface IGeolocationService
{
IResult GetGeolocationForClient(HttpContext httpContext);
IResult GetGeolocationForIp(string ipAddress);
}
1 change: 1 addition & 0 deletions src/IpSimple.PublicIp.Api/Services/IIpAddressService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ public interface IIpAddressService
{
IResult GetClientIp(HttpContext httpContext, bool getAllXForwardedForIpAddresses = false);
IResult GetAllClientIps(HttpContext httpContext);
IResult GetClientIps(HttpContext httpContext);
IResult GetClientIpv4(HttpContext httpContext, bool getAllXForwardedForIpAddresses = false);
IResult GetAllClientIpv4s(HttpContext httpContext);
IResult GetClientIpv6(HttpContext httpContext, bool getAllXForwardedForIpAddresses = false);
Expand Down
20 changes: 20 additions & 0 deletions src/IpSimple.PublicIp.Api/Services/IpAddressService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using IpSimple.Domain;
using IpSimple.Domain.Settings;
using IpSimple.Extensions;
using System.Linq;

namespace IpSimple.PublicIp.Api.Services;

Expand All @@ -23,6 +24,25 @@ public IResult GetClientIp(HttpContext httpContext, bool getAllXForwardedForIpAd

public IResult GetAllClientIps(HttpContext httpContext) => GetClientIp(httpContext, true);

public IResult GetClientIps(HttpContext httpContext)
{
// Combine IPv4 and IPv6 detection using existing helpers
// so callers can retrieve both addresses in a single call
var ipv4 = httpContext.GetClientIpv4Address();
var ipv6 = httpContext.GetClientIpv6Address();

var format = httpContext.Request.Query["format"].ToString();

if (string.Equals(format, "json", StringComparison.OrdinalIgnoreCase))
{
// TODO: extend model to match issue #5 specification
return Results.Json(new { ipv4, ipv6 }, JsonSerializerSettings.DefaultJsonSerializer, "application/json");
}

var combined = string.Join(", ", new[] { ipv4, ipv6 }.Where(s => !string.IsNullOrWhiteSpace(s)));
return Results.Text(combined, "text/plain");
}

public IResult GetClientIpv4(HttpContext httpContext, bool getAllXForwardedForIpAddresses = false)
{
var clientIp = getAllXForwardedForIpAddresses ? httpContext.GetAllPossibleClientIpv4Addresses() : httpContext.GetClientIpv4Address();
Expand Down
109 changes: 109 additions & 0 deletions src/IpSimple.PublicIp.Api/api-spec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,112 @@ paths:
example: ["2a00:1450:400f:80d::200e", "2a00:1450:400f:80d::200f"]
'429':
description: Too many requests
/all:
get:
summary: Dual stack IP addresses
description: Returns both IPv4 and IPv6 addresses if available.
parameters:
- in: query
name: format
schema:
type: string
enum: [json, plain]
required: false
responses:
'200':
description: Successful response
content:
text/plain:
schema:
type: string
example: "203.0.113.1, 2001:db8::1"
application/json:
schema:
type: object
properties:
ipv4:
type: string
example: "203.0.113.1"
ipv6:
type: string
example: "2001:db8::1"
'429':
description: Too many requests
/geolocation:
get:
summary: Geolocation for client IP
description: Returns geolocation metadata for the calling client.
responses:
'200':
description: Successful response
content:
application/json:
schema:
type: object
example: {"message": "Geolocation lookup not yet implemented"}
/geolocation/{ipAddress}:
get:
summary: Geolocation lookup
description: Provides geolocation metadata for a specific IP address.
parameters:
- in: path
name: ipAddress
required: true
schema:
type: string
responses:
'200':
description: Successful response
content:
application/json:
schema:
type: object
example: {"message": "Geolocation lookup not yet implemented"}
/bulk:
post:
summary: Submit bulk IP processing
description: Accepts a list of IP addresses for asynchronous processing.
responses:
'202':
description: Accepted
content:
application/json:
schema:
type: object
example: {"message": "Bulk IP processing not yet implemented"}
/bulk/{jobId}:
get:
summary: Bulk job status
description: Retrieves processing status for a submitted bulk IP job.
parameters:
- in: path
name: jobId
required: true
schema:
type: string
responses:
'200':
description: Successful response
content:
application/json:
schema:
type: object
example: {"jobId": "bulk-123", "status": "pending"}
/bulk/{jobId}/results:
get:
summary: Bulk job results
description: Gets processed results for a completed bulk IP job.
parameters:
- in: path
name: jobId
required: true
schema:
type: string
responses:
'200':
description: Successful response
content:
application/json:
schema:
type: object
example: {"jobId": "bulk-123", "results": []}
Loading