feat(shorebird_code_push_api): engine download endpoint and api key verification (#17)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:shelf/shelf.dart';
|
||||
@@ -12,10 +13,19 @@ Future<void> main() async {
|
||||
..all('/', (_) => Response(HttpStatus.noContent))
|
||||
..post('/api/v1/updates', checkForUpdatesHandler)
|
||||
..get('/api/v1/releases/<version>', downloadReleaseHandler)
|
||||
..post('/api/v1/releases', uploadReleaseHandler);
|
||||
..post('/api/v1/releases', uploadReleaseHandler)
|
||||
..get('/api/v1/engines/<revision>', downloadEngineHandler);
|
||||
|
||||
final apiKeys = json.decode(
|
||||
Platform.environment['CODE_PUSH_API_KEYS'] ?? '[]',
|
||||
) as List;
|
||||
|
||||
final gcpKey = Platform.environment['GCP_SA'] ?? '';
|
||||
|
||||
final handler = const Pipeline()
|
||||
.addMiddleware(versionStoreProvider)
|
||||
.addMiddleware(httpClientProvider(gcpKey))
|
||||
.addMiddleware(apiKeyVerifier(keys: apiKeys.cast<String>()))
|
||||
.addHandler(router.call);
|
||||
|
||||
final server = await shelf_io.serve(
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:googleapis_auth/auth_io.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
const _scopes = [
|
||||
// Cloud Storage
|
||||
'https://www.googleapis.com/auth/devstorage.read_write',
|
||||
];
|
||||
|
||||
Future<http.Client> createClient(String key) async {
|
||||
try {
|
||||
// coverage:ignore-start
|
||||
final serviceAccount = ServiceAccountCredentials.fromJson(key);
|
||||
final client = await clientViaServiceAccount(serviceAccount, _scopes);
|
||||
return client;
|
||||
// coverage:ignore-end
|
||||
} catch (_) {
|
||||
return http.Client();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:shelf/shelf.dart';
|
||||
|
||||
Middleware apiKeyVerifier({List<String> keys = const []}) {
|
||||
return (handler) {
|
||||
return (request) async {
|
||||
final apiKey = request.headers['x-api-key'];
|
||||
if (!keys.contains(apiKey)) return Response(HttpStatus.unauthorized);
|
||||
return handler(request);
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shorebird_code_push_api/src/http_client.dart';
|
||||
import 'package:shorebird_code_push_api/src/provider.dart';
|
||||
|
||||
Middleware httpClientProvider(String key) {
|
||||
return provider<Future<http.Client>>(
|
||||
(_) async => _httpClient ??= createClient(key),
|
||||
);
|
||||
}
|
||||
|
||||
Future<http.Client>? _httpClient;
|
||||
@@ -1 +1,3 @@
|
||||
export 'api_key_verifier.dart';
|
||||
export 'http_client_provider.dart';
|
||||
export 'version_store_provider.dart';
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export 'download_engine_handler.dart';
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shorebird_code_push_api/src/provider.dart';
|
||||
|
||||
final _engineUrl = Uri.parse(
|
||||
'https://storage.googleapis.com/download/storage/v1/b/shorebird-code-push-api.appspot.com/o/${Uri.encodeComponent('engines/engine.zip')}?alt=media',
|
||||
);
|
||||
|
||||
Future<Response> downloadEngineHandler(Request request, String revision) async {
|
||||
final httpClient = await request.lookup<Future<http.Client>>();
|
||||
final response = await httpClient.get(
|
||||
_engineUrl,
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Connection': 'close'
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode != HttpStatus.ok) {
|
||||
return Response(response.statusCode, body: response.body);
|
||||
}
|
||||
|
||||
return Response.ok(response.bodyBytes);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export 'check_for_updates/check_for_updates.dart';
|
||||
export 'download_engine/download_engine.dart';
|
||||
export 'download_release/download_release.dart';
|
||||
export 'upload_release/upload_release.dart';
|
||||
|
||||
@@ -7,6 +7,8 @@ environment:
|
||||
sdk: ">=2.19.0 <3.0.0"
|
||||
|
||||
dependencies:
|
||||
googleapis_auth: ^1.3.1
|
||||
http: ^0.13.5
|
||||
path: ^1.8.3
|
||||
shelf: ^1.4.0
|
||||
shelf_multipart: ^1.0.0
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shorebird_code_push_api/src/middleware/middleware.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('apiKeyVerifier', () {
|
||||
const keys = ['valid-key'];
|
||||
|
||||
test('returns 401 if no key is provided', () async {
|
||||
final handler = const Pipeline()
|
||||
.addMiddleware(apiKeyVerifier())
|
||||
.addHandler((_) => Response.ok('OK'));
|
||||
|
||||
final request = Request('GET', Uri.parse('http://localhost/'));
|
||||
final response = await handler(request);
|
||||
expect(response.statusCode, equals(HttpStatus.unauthorized));
|
||||
});
|
||||
|
||||
test('returns 401 if key is invalid', () async {
|
||||
final handler = const Pipeline()
|
||||
.addMiddleware(apiKeyVerifier(keys: keys))
|
||||
.addHandler((_) => Response.ok('OK'));
|
||||
|
||||
final request = Request(
|
||||
'GET',
|
||||
Uri.parse('http://localhost/'),
|
||||
headers: {'x-api-key': 'invalid-key'},
|
||||
);
|
||||
final response = await handler(request);
|
||||
expect(response.statusCode, equals(HttpStatus.unauthorized));
|
||||
});
|
||||
|
||||
test('returns 200 if key is valid', () async {
|
||||
final handler = const Pipeline()
|
||||
.addMiddleware(apiKeyVerifier(keys: keys))
|
||||
.addHandler((_) => Response.ok('OK'));
|
||||
|
||||
final request = Request(
|
||||
'GET',
|
||||
Uri.parse('http://localhost/'),
|
||||
headers: {'x-api-key': 'valid-key'},
|
||||
);
|
||||
final response = await handler(request);
|
||||
expect(response.statusCode, equals(HttpStatus.ok));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shorebird_code_push_api/src/middleware/middleware.dart';
|
||||
import 'package:shorebird_code_push_api/src/provider.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('httpClientProvider', () {
|
||||
test('provides an http client instance', () async {
|
||||
Future<http.Client>? client;
|
||||
|
||||
final handler = httpClientProvider('')(
|
||||
(req) async {
|
||||
client = req.lookup<Future<http.Client>>();
|
||||
return Response.ok('');
|
||||
},
|
||||
);
|
||||
final request = Request('GET', Uri.parse('http://localhost/'));
|
||||
|
||||
await handler(request);
|
||||
expect(client, isNotNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shorebird_code_push_api/src/provider.dart';
|
||||
import 'package:shorebird_code_push_api/src/routes/download_engine/download_engine.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class _MockHttpClient extends Mock implements http.Client {}
|
||||
|
||||
void main() {
|
||||
group('downloadEngineHandler', () {
|
||||
final uri = Uri.parse('http://localhost/');
|
||||
late http.Client httpClient;
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(Uri());
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
httpClient = _MockHttpClient();
|
||||
});
|
||||
|
||||
test('returns error on failure', () async {
|
||||
when(
|
||||
() => httpClient.get(any(), headers: any(named: 'headers')),
|
||||
).thenAnswer((_) async => http.Response('oops', HttpStatus.unauthorized));
|
||||
final request = Request('GET', uri).provide(() async => httpClient);
|
||||
|
||||
final response = await downloadEngineHandler(request, 'revision');
|
||||
expect(response.statusCode, equals(HttpStatus.unauthorized));
|
||||
});
|
||||
|
||||
test('returns bytes on success', () async {
|
||||
when(
|
||||
() => httpClient.get(any(), headers: any(named: 'headers')),
|
||||
).thenAnswer((_) async => http.Response('OK', HttpStatus.ok));
|
||||
final request = Request('GET', uri).provide(() async => httpClient);
|
||||
|
||||
final response = await downloadEngineHandler(request, 'revision');
|
||||
expect(response.statusCode, equals(HttpStatus.ok));
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user