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
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
MASTER_API_KEY="SOME_API_KEY"
MASTER_API_KEY="SOME_API_KEY"
CONFIG_FILE="./config/example.json"
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ You can run the example with the [Dart SDK](https://dart.dev/get-dart)
like this:

```text
$ dart run bin/server.dart
$ dart run bin/appitoolbox.dart
Server listening on port 8080
```

Expand Down
37 changes: 7 additions & 30 deletions bin/api_handler.dart
Original file line number Diff line number Diff line change
@@ -1,21 +1,10 @@
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
import 'package:http/http.dart' as http;
import 'package:dotenv/dotenv.dart' as dotenv;
import 'package:shelf_plus/shelf_plus.dart';

import 'api_middleware.dart';

class ApiHandler {
Future<Response> _universalGetHandler(Request request) async {
if (request.headers['X-AppiToolbox-ApiKey'] == null) {
return Response.forbidden(
'No API Key provided. Use header X-AppiToolbox-ApiKey');
}
if (request.headers['X-AppiToolbox-ApiKey'] !=
dotenv.env['MASTER_API_KEY']) {
return Response.forbidden('Wrong value of X-AppiToolbox-ApiKey');
}
if (request.params['url'] == null) {
return Response.notFound("You must supply valid URL.");
}
Uri url = Uri.parse("https://" + request.params['url']!);
print("got url");
Map<String, String> headers = Map.from(request.headers);
Expand All @@ -37,18 +26,6 @@ class ApiHandler {
}

Future<Response> _universalPostHandler(Request request) async {
if (request.headers['X-AppiToolbox-ApiKey'] == null) {
return Response.forbidden(
'No API Key provided. Use header X-AppiToolbox-ApiKey');
}
if (request.headers['X-AppiToolbox-ApiKey'] !=
dotenv.env['MASTER_API_KEY']) {
return Response.forbidden('Wrong value of X-AppiToolbox-ApiKey');
}

if (request.params['url'] == null) {
return Response.notFound("You must supply valid URL.");
}
Uri url = Uri.parse("https://" + request.params['url']!);
Map<String, String> headers = Map.from(request.headers);
print(headers);
Expand Down Expand Up @@ -76,12 +53,12 @@ class ApiHandler {

// By exposing a [Router] for an object, it can be mounted in other routers.
Router get router {
final router = Router();
final router = Router().plus;

router
..get('/<url|.*>', _universalGetHandler)
..post('/<url|.*>', _universalPostHandler);
..get('/<url|.*>', _universalGetHandler, use: apiMiddleware())
..post('/<url|.*>', _universalPostHandler, use: apiMiddleware());

return router;
return router.shelfRouter;
}
}
59 changes: 59 additions & 0 deletions bin/api_middleware.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import 'dart:async';

import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
import 'model/status.dart';
import 'utils/globals.dart' as globals;
import 'package:dotenv/dotenv.dart' as dotenv;

Middleware apiMiddleware({
FutureOr<Response?> Function(Request)? requestHandler,
FutureOr<Response> Function(Response)? responseHandler,
FutureOr<Response> Function(Object error, StackTrace)? errorHandler,
}) {
requestHandler ??= (request) => null;
responseHandler ??= (response) => response;

FutureOr<Response> Function(Object, StackTrace)? onError;
if (errorHandler != null) {
onError = (error, stackTrace) {
if (error is HijackException) throw error;
return errorHandler(error, stackTrace);
};
}

return (Handler innerHandler) {
return (request) {
if (request.headers['X-AppiToolbox-ApiKey'] == null) {
return Response.forbidden(
'No API Key provided. Use header X-AppiToolbox-ApiKey');
}
if (request.headers['X-AppiToolbox-ApiKey'] !=
dotenv.env['MASTER_API_KEY']) {
return Response.forbidden('Wrong value of X-AppiToolbox-ApiKey');
}
if (request.params['url'] == null) {
return Response.notFound("You must supply valid URL.");
}

String requestedEndpoint = request.params['url']!;
Status status = globals.endpointConfigService
.endpointStatus(requestedEndpoint, request.method);

if (status == Status.undefined) {
status = globals.globalConfigService.defaultStatus(request.method);
}

if (status == Status.blocked) {
return Response.forbidden("This endpoint is blocked.");
}

return Future.sync(() => requestHandler!(request)).then((response) {
if (response != null) return response;

return Future.sync(() => innerHandler(request))
.then((response) => responseHandler!(response), onError: onError);
});
};
};
}
3 changes: 3 additions & 0 deletions bin/appitoolbox.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart';
import 'package:shelf_router/shelf_router.dart';
import 'package:dotenv/dotenv.dart' as dotenv;
import 'utils/globals.dart' as globals;

import 'api_handler.dart';

Expand Down Expand Up @@ -61,6 +62,8 @@ void main(List<String> args) async {
final port = int.parse(Platform.environment['PORT'] ?? '8080');
final server = await serve(_handler, ip, port);
dotenv.load();
globals.endpointConfigService.init();
globals.globalConfigService.init();
print('Server listening on port ${server.port}');

ProcessSignal.sigint.watch().listen((ProcessSignal signal) {
Expand Down
73 changes: 73 additions & 0 deletions bin/model/endpoint_config.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import 'method.dart';
import 'status.dart';

class EndpointConfig {
String? title;
String? url;
Method method;
Status status;
bool isDefault;

EndpointConfig(
{this.title,
this.url,
this.method = Method.undefined,
this.status = Status.undefined,
this.isDefault = false});

factory EndpointConfig.fromJson(Map<String, dynamic> json,
{bool isDefault = false}) {
late Status status;
late Method method;
if (json['title'] == null && !isDefault) {
throw Exception("Title is required");
}
if (json['url'] == null && !isDefault) {
throw Exception("URL is required");
}
if (json['method'] == null) {
method = Method.undefined;
} else {
try {
method = Method.values.byName(json['method']!);
} catch (e) {
method = Method.undefined;
}
}
if (json['status'] == null) {
status = Status.undefined;
} else {
try {
status = Status.values.byName(json['status']!);
} catch (e) {
status = Status.undefined;
}
}

return EndpointConfig(
title: json['title'],
url: json['url'],
method: method,
status: status,
isDefault: isDefault);
}

Status? getStatusFromString(String status) {
status = 'Status.$status';
return Status.values.firstWhere((f) => f.toString() == status,
orElse: () => Status.undefined);
}

Method? getMethodFromString(String method) {
method = 'Method.$method';
return Method.values.firstWhere((f) => f.toString() == method,
orElse: () => Method.undefined);
}

Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['title'] = title;
data['url'] = url;
return data;
}
}
33 changes: 33 additions & 0 deletions bin/model/global_config.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import 'endpoint_config.dart';
import 'status.dart';

class GlobalConfig {
Status defaultStatus;
List<EndpointConfig> defaultEndpoints;

GlobalConfig({
required this.defaultStatus,
required this.defaultEndpoints,
});

factory GlobalConfig.fromJson(Map<String, dynamic> json) {
Status defaultStatus;
if (json['defaultStatus'] == null) {
throw Exception("defaultStatus is required");
} else {
try {
defaultStatus = Status.values.byName(json['defaultStatus']!);
} catch (e) {
throw Exception("defaultStatus have invalid value");
}
}

return GlobalConfig(
defaultStatus: defaultStatus,
defaultEndpoints: (json['defaultEndpoints'] as List<dynamic>)
.map((e) => EndpointConfig.fromJson(e as Map<String, dynamic>,
isDefault: true))
.toList(),
);
}
}
13 changes: 13 additions & 0 deletions bin/model/method.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
enum Method {
get,
head,
post,
put,
delete,
connect,
options,
trace,
patch,
undefined,
all
}
1 change: 1 addition & 0 deletions bin/model/status.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
enum Status { blocked, allowed, undefined }
54 changes: 54 additions & 0 deletions bin/services/endpoint_config.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import '../model/endpoint_config.dart';
import '../model/method.dart';
import '../model/status.dart';
import '../utils/globals.dart' as globals;

class EndpointConfigService {
List<EndpointConfig> configs = [];
bool loaded = false;

Future init() async {
await _loadEndpoints();
loaded = true;
}

Future _loadEndpoints() async {
if (!globals.configLoaded) {
await globals.loadConfig();
}
final json = globals.config;
for (var item in json["endpoints"]) {
configs.add(EndpointConfig.fromJson(item));
}
return true;
}

Status endpointStatus(String url, String methodS) {
Method method;
try {
method = Method.values.byName(methodS.toLowerCase());
} catch (e) {
method = Method.undefined;
}
if (!loaded) {
throw Exception("You need to load config first");
}
for (var item in configs) {
if (item.method != method &&
item.method != Method.undefined &&
item.method != Method.all) {
break;
}
if (item.url == url) {
return item.status;
}
if (item.url == "https://" + url) {
return item.status;
}
if (item.url == "http://" + url) {
return item.status;
}
}
return Status.undefined;
}
}
44 changes: 44 additions & 0 deletions bin/services/global_config.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import 'dart:io';

import '../model/global_config.dart';
import '../model/method.dart';
import '../model/status.dart';
import '../utils/globals.dart' as globals;

class GlobalConfigService {
late GlobalConfig globalConfig;
var defaultConfig = File('./config/example.json');
bool loaded = false;

Future init() async {
await loadConfig();
loaded = true;
}

Future loadConfig() async {
if (!globals.configLoaded) {
await globals.loadConfig();
}
final json = globals.config;
globalConfig = GlobalConfig.fromJson(json["global"]);
}

Status defaultStatus(String methodS) {
Method method;
try {
method = Method.values.byName(methodS.toLowerCase());
} catch (e) {
method = Method.undefined;
}
if (!loaded) {
throw Exception("You need to load config first");
}
try {
return globalConfig.defaultEndpoints
.firstWhere((endpoint) => endpoint.method == method)
.status;
} catch (e) {
return globalConfig.defaultStatus;
}
}
}
18 changes: 18 additions & 0 deletions bin/utils/globals.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import 'dart:convert';
import 'dart:io';

import '../services/endpoint_config.dart';
import '../services/global_config.dart';
import 'package:dotenv/dotenv.dart' as dotenv;

EndpointConfigService endpointConfigService = EndpointConfigService();
GlobalConfigService globalConfigService = GlobalConfigService();
late Map config;
bool configLoaded = false;

loadConfig() async {
String configPath = dotenv.env['CONFIG_FILE'] ?? "./config/example.json";
File configFile = File(configPath);
config = jsonDecode(await configFile.readAsString());
configLoaded = true;
}
Loading