From 79fad38c8b044d890287a689cd2d44cbda73a2ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Fri, 6 Aug 2021 11:54:28 +0300 Subject: [PATCH] refactored --- etc/k8s/README.md | 2 + etc/k8s/helm-chart/eventhub/Chart.yaml | 2 +- .../account/templates/account-ingress.yaml | 2 +- .../eventhub/charts/account/values.yaml | 1 - .../eventhub/charts/api/values.yaml | 1 - .../eventhub/charts/dbmigrator/Chart.yaml | 2 +- .../eventhub/charts/dbmigrator/values.yaml | 1 - .../eventhub/charts/www/values.yaml | 1 - etc/k8s/helm-chart/eventhub/values.yaml | 10 ++- .../EventHubAdminHttpApiHostModule.cs | 16 +++-- ...eSiteCookiesServiceCollectionExtensions.cs | 71 +++++++++++++++++++ .../appsettings.json | 9 +-- src/EventHub.Domain/EventHubDomainModule.cs | 2 - .../EventHubHttpApiHostModule.cs | 10 ++- ...eSiteCookiesServiceCollectionExtensions.cs | 3 +- .../EventHub.IdentityServer.csproj | 4 +- .../EventHubIdentityServerModule.cs | 22 +++--- ...eSiteCookiesServiceCollectionExtensions.cs | 3 +- src/EventHub.Web/EventHubWebModule.cs | 8 +++ ...eSiteCookiesServiceCollectionExtensions.cs | 71 +++++++++++++++++++ 20 files changed, 197 insertions(+), 44 deletions(-) create mode 100644 src/EventHub.Admin.HttpApi.Host/Utils/SameSiteCookiesServiceCollectionExtensions.cs rename src/EventHub.HttpApi.Host/{ => Utils}/SameSiteCookiesServiceCollectionExtensions.cs (97%) rename src/EventHub.IdentityServer/{ => Utils}/SameSiteCookiesServiceCollectionExtensions.cs (97%) create mode 100644 src/EventHub.Web/Utils/SameSiteCookiesServiceCollectionExtensions.cs diff --git a/etc/k8s/README.md b/etc/k8s/README.md index 9c67fd6..c40bbf5 100644 --- a/etc/k8s/README.md +++ b/etc/k8s/README.md @@ -6,6 +6,8 @@ 127.0.0.1 eh-st-account 127.0.0.1 eh-st-www 127.0.0.1 eh-st-api +127.0.0.1 eh-st-admin +127.0.0.1 eh-st-admin-api ```` * Run `build-images.ps1` in the `scripts` directory. diff --git a/etc/k8s/helm-chart/eventhub/Chart.yaml b/etc/k8s/helm-chart/eventhub/Chart.yaml index 5c0b700..ba07319 100644 --- a/etc/k8s/helm-chart/eventhub/Chart.yaml +++ b/etc/k8s/helm-chart/eventhub/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v2 name: eventhub appVersion: "1.0" -description: Eventhub application chart +description: EventHub solution version: 1.0.0 type: application diff --git a/etc/k8s/helm-chart/eventhub/charts/account/templates/account-ingress.yaml b/etc/k8s/helm-chart/eventhub/charts/account/templates/account-ingress.yaml index 7180782..d99fb57 100644 --- a/etc/k8s/helm-chart/eventhub/charts/account/templates/account-ingress.yaml +++ b/etc/k8s/helm-chart/eventhub/charts/account/templates/account-ingress.yaml @@ -8,7 +8,7 @@ metadata: nginx.ingress.kubernetes.io/proxy-buffer-size: "{{ .Values.global.nginxProxyBufferSize }}" nginx.ingress.kubernetes.io/proxy-buffers-number: "{{ .Values.global.nginxProxyBuffersNumber }}" nginx.ingress.kubernetes.io/configuration-snippet: | - more_set_input_headers "fromingress: true"; + more_set_input_headers "from-ingress: true"; spec: tls: - hosts: diff --git a/etc/k8s/helm-chart/eventhub/charts/account/values.yaml b/etc/k8s/helm-chart/eventhub/charts/account/values.yaml index 00bc93b..e69de29 100644 --- a/etc/k8s/helm-chart/eventhub/charts/account/values.yaml +++ b/etc/k8s/helm-chart/eventhub/charts/account/values.yaml @@ -1 +0,0 @@ -containerImage: "eventhub.account" \ No newline at end of file diff --git a/etc/k8s/helm-chart/eventhub/charts/api/values.yaml b/etc/k8s/helm-chart/eventhub/charts/api/values.yaml index 7309132..e69de29 100644 --- a/etc/k8s/helm-chart/eventhub/charts/api/values.yaml +++ b/etc/k8s/helm-chart/eventhub/charts/api/values.yaml @@ -1 +0,0 @@ -containerImage: "eventhub.api" \ No newline at end of file diff --git a/etc/k8s/helm-chart/eventhub/charts/dbmigrator/Chart.yaml b/etc/k8s/helm-chart/eventhub/charts/dbmigrator/Chart.yaml index a20b761..fa640cd 100644 --- a/etc/k8s/helm-chart/eventhub/charts/dbmigrator/Chart.yaml +++ b/etc/k8s/helm-chart/eventhub/charts/dbmigrator/Chart.yaml @@ -1,5 +1,5 @@ apiVersion: v2 -name: migrator +name: dbmigrator appVersion: "1.0" description: EventHub Migrator Application version: 1.0.0 diff --git a/etc/k8s/helm-chart/eventhub/charts/dbmigrator/values.yaml b/etc/k8s/helm-chart/eventhub/charts/dbmigrator/values.yaml index 92cb5f6..e69de29 100644 --- a/etc/k8s/helm-chart/eventhub/charts/dbmigrator/values.yaml +++ b/etc/k8s/helm-chart/eventhub/charts/dbmigrator/values.yaml @@ -1 +0,0 @@ -containerImage: eventhub.dbmigrator \ No newline at end of file diff --git a/etc/k8s/helm-chart/eventhub/charts/www/values.yaml b/etc/k8s/helm-chart/eventhub/charts/www/values.yaml index 7d204b0..e69de29 100644 --- a/etc/k8s/helm-chart/eventhub/charts/www/values.yaml +++ b/etc/k8s/helm-chart/eventhub/charts/www/values.yaml @@ -1 +0,0 @@ -containerImage: eventhub.www \ No newline at end of file diff --git a/etc/k8s/helm-chart/eventhub/values.yaml b/etc/k8s/helm-chart/eventhub/values.yaml index 5cf6e4e..9d122ce 100644 --- a/etc/k8s/helm-chart/eventhub/values.yaml +++ b/etc/k8s/helm-chart/eventhub/values.yaml @@ -15,4 +15,12 @@ global: redisConfiguration: "eh-st-redis" internalAuthServerAuthority: "http://eh-st-account" internalAuthServerRequireHttpsMetadata: "false" - stringEncryptionDefaultPassPhrase: "TxVIZFPxK33czbbv" \ No newline at end of file + stringEncryptionDefaultPassPhrase: "TxVIZFPxK33czbbv" +account: + containerImage: "eventhub.account" +api: + containerImage: "eventhub.api" +www: + containerImage: "eventhub.www" +dbmigrator: + containerImage: "eventhub.dbmigrator" \ No newline at end of file diff --git a/src/EventHub.Admin.HttpApi.Host/EventHubAdminHttpApiHostModule.cs b/src/EventHub.Admin.HttpApi.Host/EventHubAdminHttpApiHostModule.cs index 38ff481..0137118 100644 --- a/src/EventHub.Admin.HttpApi.Host/EventHubAdminHttpApiHostModule.cs +++ b/src/EventHub.Admin.HttpApi.Host/EventHubAdminHttpApiHostModule.cs @@ -3,7 +3,9 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using EventHub.Admin.Utils; using EventHub.EntityFrameworkCore; +using EventHub.Web; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Cors; @@ -54,6 +56,7 @@ namespace EventHub.Admin ConfigureVirtualFileSystem(context); ConfigureRedis(context, configuration); ConfigureCors(context, configuration); + ConfigureCookies(context); ConfigureSwaggerServices(context, configuration); ConfigureBackgroundJobs(); } @@ -148,10 +151,7 @@ namespace EventHub.Admin { builder .WithOrigins( - configuration["App:CorsOrigins"] - .Split(",", StringSplitOptions.RemoveEmptyEntries) - .Select(o => o.RemovePostFix("/")) - .ToArray() + configuration[EventHubUrlOptions.GetAdminConfigKey()] ) .WithAbpExposedHeaders() .SetIsOriginAllowedToAllowWildcardSubdomains() @@ -162,6 +162,11 @@ namespace EventHub.Admin }); } + private void ConfigureCookies(ServiceConfigurationContext context) + { + context.Services.AddSameSiteCookiePolicy(); + } + public override void OnApplicationInitialization(ApplicationInitializationContext context) { var app = context.GetApplicationBuilder(); @@ -194,8 +199,9 @@ namespace EventHub.Admin app.UseErrorPage(); } + app.UseCookiePolicy(); app.UseCorrelationId(); - app.UseVirtualFiles(); + app.UseStaticFiles(); app.UseRouting(); app.UseCors(DefaultCorsPolicyName); app.UseAuthentication(); diff --git a/src/EventHub.Admin.HttpApi.Host/Utils/SameSiteCookiesServiceCollectionExtensions.cs b/src/EventHub.Admin.HttpApi.Host/Utils/SameSiteCookiesServiceCollectionExtensions.cs new file mode 100644 index 0000000..0895c29 --- /dev/null +++ b/src/EventHub.Admin.HttpApi.Host/Utils/SameSiteCookiesServiceCollectionExtensions.cs @@ -0,0 +1,71 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace EventHub.Admin.Utils +{ + public static class SameSiteCookiesServiceCollectionExtensions + { + public static IServiceCollection AddSameSiteCookiePolicy(this IServiceCollection services) + { + services.Configure(options => + { + options.MinimumSameSitePolicy = SameSiteMode.Unspecified; + options.OnAppendCookie = cookieContext => + CheckSameSite(cookieContext.Context, cookieContext.CookieOptions); + options.OnDeleteCookie = cookieContext => + CheckSameSite(cookieContext.Context, cookieContext.CookieOptions); + }); + + return services; + } + + private static void CheckSameSite(HttpContext httpContext, CookieOptions options) + { + if (options.SameSite == SameSiteMode.None) + { + var userAgent = httpContext.Request.Headers["User-Agent"].ToString(); + if (!httpContext.Request.IsHttps || DisallowsSameSiteNone(userAgent)) + { + // For .NET Core < 3.1 set SameSite = (SameSiteMode)(-1) + options.SameSite = SameSiteMode.Unspecified; + } + } + } + + private static bool DisallowsSameSiteNone(string userAgent) + { + // Cover all iOS based browsers here. This includes: + // - Safari on iOS 12 for iPhone, iPod Touch, iPad + // - WkWebview on iOS 12 for iPhone, iPod Touch, iPad + // - Chrome on iOS 12 for iPhone, iPod Touch, iPad + // All of which are broken by SameSite=None, because they use the iOS networking stack + if (userAgent.Contains("CPU iPhone OS 12") || userAgent.Contains("iPad; CPU OS 12")) + { + return true; + } + + // Cover Mac OS X based browsers that use the Mac OS networking stack. This includes: + // - Safari on Mac OS X. + // This does not include: + // - Chrome on Mac OS X + // Because they do not use the Mac OS networking stack. + if (userAgent.Contains("Macintosh; Intel Mac OS X 10_14") && + userAgent.Contains("Version/") && userAgent.Contains("Safari")) + { + return true; + } + + // Cover Chrome 50-69, because some versions are broken by SameSite=None, + // and none in this range require it. + // Note: this covers some pre-Chromium Edge versions, + // but pre-Chromium Edge does not require SameSite=None. + if (userAgent.Contains("Chrome/5") || userAgent.Contains("Chrome/6")) + { + return true; + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/EventHub.Admin.HttpApi.Host/appsettings.json b/src/EventHub.Admin.HttpApi.Host/appsettings.json index 61adbd3..aca9ac1 100644 --- a/src/EventHub.Admin.HttpApi.Host/appsettings.json +++ b/src/EventHub.Admin.HttpApi.Host/appsettings.json @@ -1,21 +1,14 @@ { - "App": { - "CorsOrigins": "https://*.openeventhub.com,https://localhost:44307,https://localhost:44308", - "WebAppUrl": "https://eh-st-www" - }, "ConnectionStrings": { "Default": "Host=localhost;Database=EventHub;Username=root;Password=root;Port=5432" }, "Redis": { - "Configuration": "127.0.0.1" + "Configuration": "localhost" }, "AuthServer": { "Authority": "https://localhost:44313", "RequireHttpsMetadata": "true", "SwaggerClientId": "EventHub_Swagger", "SwaggerClientSecret": "1q2w3e*" - }, - "StringEncryption": { - "DefaultPassPhrase": "TxVIZFPxK33czbbv" } } diff --git a/src/EventHub.Domain/EventHubDomainModule.cs b/src/EventHub.Domain/EventHubDomainModule.cs index cd34153..8c17cf5 100644 --- a/src/EventHub.Domain/EventHubDomainModule.cs +++ b/src/EventHub.Domain/EventHubDomainModule.cs @@ -1,5 +1,3 @@ -using EventHub.Emailing; -using EventHub.Web; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Volo.Abp.AuditLogging; diff --git a/src/EventHub.HttpApi.Host/EventHubHttpApiHostModule.cs b/src/EventHub.HttpApi.Host/EventHubHttpApiHostModule.cs index 6bc3a7b..9345194 100644 --- a/src/EventHub.HttpApi.Host/EventHubHttpApiHostModule.cs +++ b/src/EventHub.HttpApi.Host/EventHubHttpApiHostModule.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using EventHub.EntityFrameworkCore; +using EventHub.Utils; using EventHub.Web; using Microsoft.AspNetCore.Localization; using StackExchange.Redis; @@ -58,6 +59,7 @@ namespace EventHub ConfigureVirtualFileSystem(context); ConfigureRedis(context, configuration); ConfigureCors(context, configuration); + ConfigureCookies(context); ConfigureSwaggerServices(context, configuration); ConfigureBackgroundJobs(); } @@ -169,7 +171,10 @@ namespace EventHub .AllowCredentials(); }); }); - + } + + private void ConfigureCookies(ServiceConfigurationContext context) + { context.Services.AddSameSiteCookiePolicy(); } @@ -212,9 +217,8 @@ namespace EventHub } app.UseCookiePolicy(); - app.UseCorrelationId(); - app.UseVirtualFiles(); + app.UseStaticFiles(); app.UseRouting(); app.UseCors(DefaultCorsPolicyName); app.UseAuthentication(); diff --git a/src/EventHub.HttpApi.Host/SameSiteCookiesServiceCollectionExtensions.cs b/src/EventHub.HttpApi.Host/Utils/SameSiteCookiesServiceCollectionExtensions.cs similarity index 97% rename from src/EventHub.HttpApi.Host/SameSiteCookiesServiceCollectionExtensions.cs rename to src/EventHub.HttpApi.Host/Utils/SameSiteCookiesServiceCollectionExtensions.cs index 3b0c754..2b35fe2 100644 --- a/src/EventHub.HttpApi.Host/SameSiteCookiesServiceCollectionExtensions.cs +++ b/src/EventHub.HttpApi.Host/Utils/SameSiteCookiesServiceCollectionExtensions.cs @@ -1,7 +1,8 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.Extensions.DependencyInjection +namespace EventHub.Utils { public static class SameSiteCookiesServiceCollectionExtensions { diff --git a/src/EventHub.IdentityServer/EventHub.IdentityServer.csproj b/src/EventHub.IdentityServer/EventHub.IdentityServer.csproj index 7875836..dce5b30 100644 --- a/src/EventHub.IdentityServer/EventHub.IdentityServer.csproj +++ b/src/EventHub.IdentityServer/EventHub.IdentityServer.csproj @@ -20,8 +20,8 @@ - - + + Always diff --git a/src/EventHub.IdentityServer/EventHubIdentityServerModule.cs b/src/EventHub.IdentityServer/EventHubIdentityServerModule.cs index da0fb56..55d63bc 100644 --- a/src/EventHub.IdentityServer/EventHubIdentityServerModule.cs +++ b/src/EventHub.IdentityServer/EventHubIdentityServerModule.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using EventHub.EntityFrameworkCore; using EventHub.Localization; +using EventHub.Utils; using EventHub.Web; using EventHub.Web.Theme; using EventHub.Web.Theme.Bundling; @@ -70,8 +71,8 @@ namespace EventHub private X509Certificate2 GetSigningCertificate(IWebHostEnvironment hostingEnv, IConfiguration configuration) { - var fileName = "EventHub.IdentityServer.pfx"; - var passPhrase = "e8202f07-66e5-4619-be07-72ba76fde97f"; + var fileName = "account.openeventhub.pfx"; + var passPhrase = "a8202f07-66e5-4619-be07-72ba76fde97f"; var file = Path.Combine(hostingEnv.ContentRootPath, fileName); if (!File.Exists(file)) @@ -117,7 +118,7 @@ namespace EventHub Configure(options => { - options.IssuerUri = configuration["AppUrls:Account"]; + options.IssuerUri = configuration[EventHubUrlOptions.GetAccountConfigKey()]; }); if (hostingEnvironment.IsDevelopment()) @@ -137,7 +138,9 @@ namespace EventHub new[] { configuration[EventHubUrlOptions.GetWwwConfigKey()], - configuration[EventHubUrlOptions.GetAdminConfigKey()] + configuration[EventHubUrlOptions.GetAdminConfigKey()], + configuration[EventHubUrlOptions.GetApiConfigKey()], + configuration[EventHubUrlOptions.GetAdminApiConfigKey()] }); }); @@ -184,17 +187,9 @@ namespace EventHub var env = context.GetEnvironment(); var configuration = context.ServiceProvider.GetRequiredService(); - /* - app.Use((context, next) => - { - context.Request.Scheme = "https"; - return next(); - }); - */ - app.Use(async (ctx, next) => { - if (ctx.Request.Headers.ContainsKey("fromingress")) + if (ctx.Request.Headers.ContainsKey("from-ingress")) { ctx.SetIdentityServerOrigin(configuration[EventHubUrlOptions.GetAccountConfigKey()]); } @@ -215,7 +210,6 @@ namespace EventHub } app.UseCookiePolicy(); - app.UseCorrelationId(); app.UseStaticFiles(); app.UseRouting(); diff --git a/src/EventHub.IdentityServer/SameSiteCookiesServiceCollectionExtensions.cs b/src/EventHub.IdentityServer/Utils/SameSiteCookiesServiceCollectionExtensions.cs similarity index 97% rename from src/EventHub.IdentityServer/SameSiteCookiesServiceCollectionExtensions.cs rename to src/EventHub.IdentityServer/Utils/SameSiteCookiesServiceCollectionExtensions.cs index 3b0c754..2b35fe2 100644 --- a/src/EventHub.IdentityServer/SameSiteCookiesServiceCollectionExtensions.cs +++ b/src/EventHub.IdentityServer/Utils/SameSiteCookiesServiceCollectionExtensions.cs @@ -1,7 +1,8 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.Extensions.DependencyInjection +namespace EventHub.Utils { public static class SameSiteCookiesServiceCollectionExtensions { diff --git a/src/EventHub.Web/EventHubWebModule.cs b/src/EventHub.Web/EventHubWebModule.cs index 6f1efcd..aa72d51 100644 --- a/src/EventHub.Web/EventHubWebModule.cs +++ b/src/EventHub.Web/EventHubWebModule.cs @@ -12,6 +12,7 @@ using EventHub.Localization; using EventHub.Web.Menus; using EventHub.Web.Theme; using EventHub.Web.Theme.Bundling; +using EventHub.Web.Utils; using Microsoft.AspNetCore.Mvc.RazorPages; using StackExchange.Redis; using Microsoft.OpenApi.Models; @@ -80,6 +81,7 @@ namespace EventHub.Web ConfigureAutoMapper(); ConfigureVirtualFileSystem(hostingEnvironment); ConfigureNavigationServices(configuration); + ConfigureCookies(context); ConfigureSwaggerServices(context.Services); ConfigureRazorPageOptions(); } @@ -198,6 +200,11 @@ namespace EventHub.Web options.Contributors.Add(new EventHubToolbarContributor()); }); } + + private void ConfigureCookies(ServiceConfigurationContext context) + { + context.Services.AddSameSiteCookiePolicy(); + } private void ConfigureSwaggerServices(IServiceCollection services) { @@ -244,6 +251,7 @@ namespace EventHub.Web app.UseErrorPage(); } + app.UseCookiePolicy(); app.UseCorrelationId(); app.UseStaticFiles(); app.UseRouting(); diff --git a/src/EventHub.Web/Utils/SameSiteCookiesServiceCollectionExtensions.cs b/src/EventHub.Web/Utils/SameSiteCookiesServiceCollectionExtensions.cs new file mode 100644 index 0000000..64576c1 --- /dev/null +++ b/src/EventHub.Web/Utils/SameSiteCookiesServiceCollectionExtensions.cs @@ -0,0 +1,71 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace EventHub.Web.Utils +{ + public static class SameSiteCookiesServiceCollectionExtensions + { + public static IServiceCollection AddSameSiteCookiePolicy(this IServiceCollection services) + { + services.Configure(options => + { + options.MinimumSameSitePolicy = SameSiteMode.Unspecified; + options.OnAppendCookie = cookieContext => + CheckSameSite(cookieContext.Context, cookieContext.CookieOptions); + options.OnDeleteCookie = cookieContext => + CheckSameSite(cookieContext.Context, cookieContext.CookieOptions); + }); + + return services; + } + + private static void CheckSameSite(HttpContext httpContext, CookieOptions options) + { + if (options.SameSite == SameSiteMode.None) + { + var userAgent = httpContext.Request.Headers["User-Agent"].ToString(); + if (!httpContext.Request.IsHttps || DisallowsSameSiteNone(userAgent)) + { + // For .NET Core < 3.1 set SameSite = (SameSiteMode)(-1) + options.SameSite = SameSiteMode.Unspecified; + } + } + } + + private static bool DisallowsSameSiteNone(string userAgent) + { + // Cover all iOS based browsers here. This includes: + // - Safari on iOS 12 for iPhone, iPod Touch, iPad + // - WkWebview on iOS 12 for iPhone, iPod Touch, iPad + // - Chrome on iOS 12 for iPhone, iPod Touch, iPad + // All of which are broken by SameSite=None, because they use the iOS networking stack + if (userAgent.Contains("CPU iPhone OS 12") || userAgent.Contains("iPad; CPU OS 12")) + { + return true; + } + + // Cover Mac OS X based browsers that use the Mac OS networking stack. This includes: + // - Safari on Mac OS X. + // This does not include: + // - Chrome on Mac OS X + // Because they do not use the Mac OS networking stack. + if (userAgent.Contains("Macintosh; Intel Mac OS X 10_14") && + userAgent.Contains("Version/") && userAgent.Contains("Safari")) + { + return true; + } + + // Cover Chrome 50-69, because some versions are broken by SameSite=None, + // and none in this range require it. + // Note: this covers some pre-Chromium Edge versions, + // but pre-Chromium Edge does not require SameSite=None. + if (userAgent.Contains("Chrome/5") || userAgent.Contains("Chrome/6")) + { + return true; + } + + return false; + } + } +} \ No newline at end of file