Nice little directory browser :D
1/*
2 This file is part of Utatane.
3
4 Utatane is free software: you can redistribute it and/or modify it under
5 the terms of the GNU Affero General Public License as published by the Free
6 Software Foundation, either version 3 of the License, or (at your option)
7 any later version.
8
9 Utatane is distributed in the hope that it will be useful, but WITHOUT ANY
10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11 FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
12 more details.
13
14 You should have received a copy of the GNU Affero General Public License
15 along with Utatane. If not, see <http://www.gnu.org/licenses/>.
16*/
17
18using System.IO.Compression;
19using System.Text;
20using Microsoft.AspNetCore.Diagnostics;
21using Microsoft.AspNetCore.Http.Features;
22using Utatane.Components;
23using Microsoft.AspNetCore.ResponseCompression;
24using Microsoft.AspNetCore.Rewrite;
25using Utatane;
26
27var builder = WebApplication.CreateBuilder(new WebApplicationOptions() {
28 Args = args,
29 WebRootPath = "public"
30});
31
32// Add services to the container.
33// Add services to the container.
34builder.Services.AddRazorComponents();
35builder.Services.AddResponseCompression(options => {
36 options.EnableForHttps = true;
37 options.Providers.Add<BrotliCompressionProvider>();
38 options.Providers.Add<GzipCompressionProvider>();
39 options.MimeTypes = ResponseCompressionDefaults.MimeTypes;
40} );
41builder.Services.Configure<BrotliCompressionProviderOptions>(options => {
42 options.Level = CompressionLevel.SmallestSize;
43});
44
45builder.Services.Configure<GzipCompressionProviderOptions>(options => {
46 options.Level = CompressionLevel.SmallestSize;
47});
48
49builder.Services.AddHttpContextAccessor();
50
51var app = builder.Build();
52
53// Configure the HTTP request pipeline.
54if (!app.Environment.IsDevelopment()) {
55 app.UseExceptionHandler("/Error", createScopeForErrors: true);
56}
57
58app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
59
60app.UseAntiforgery();
61app.UseResponseCompression();
62
63// check paths exist
64app.Use(async (context, next) => {
65 context.Request.Headers.Append("X-Nhnd-Compress-Me", "false");
66 if (context.Request.Path.StartsWithSegments("/api/files")) {
67 // TODO: for future JSON response at this endpoint, set false!
68 context.Request.Headers["X-Nhnd-Compress-Me"] = "true";
69 await next(context);
70 return;
71 } else if (
72 // if reexecuting for an error, let someone else handle that
73 context.Features.Get<IStatusCodeReExecuteFeature>() != null
74 // let our static file error handler do that
75 || context.Request.Path.StartsWithSegments("/.nhnd")
76 ) {
77 await next(context);
78 return;
79 }
80
81 var resolved = Utils.VerifyPath(Uri.UnescapeDataString(context.Request.Path));
82
83 if (resolved.IsFailed) {
84 context.Response.StatusCode = StatusCodes.Status404NotFound;
85
86 var original = context.Request.Path;
87
88 var routeFeature = context.Features.Get<IRouteValuesFeature>();
89 routeFeature?.RouteValues = new RouteValueDictionary();
90 context.SetEndpoint(null);
91
92 await next(context);
93
94 context.Request.Path = original;
95 return;
96 }
97
98 // if we're a file AND we aren't a link to a directory
99 if (resolved.Value.IsT2 && resolved.Value.AsT2().UnravelLink() is not DirectoryInfo) {
100 FileInfo file = new FileInfo(resolved.Value.AsT2().UnravelLink().FullName);
101
102 await Results.File(
103 file.FullName,
104 MimeMapping.MimeUtility.GetMimeMapping(file.Name),
105 file.Name,
106 lastModified: file.LastWriteTimeUtc,
107 enableRangeProcessing: true
108 ).ExecuteAsync(context);
109
110 return;
111 }
112
113 // we are either a directory or a link to one
114 context.Request.Headers["X-Nhnd-Compress-Me"] = "true";
115 await next(context);
116});
117
118// HTML compression
119app.Use(async (context, next) => {
120 if (context.Request.Headers["X-Nhnd-Compress-Me"] == "false") {
121 await next(context);
122 return;
123 }
124
125 // context.Response.Body is a direct line to the client, so
126 // swap it out for our own in-memory stream for now
127 Stream responseStream = context.Response.Body;
128 using var memoryStream = new MemoryStream();
129 context.Response.Body = memoryStream;
130
131 // let downstream render the response & write to our stream
132 await next(context);
133
134 if (
135 context.Response.ContentType?.StartsWith("text/html") != true
136 || context.Response.Headers.ContentDisposition.Any(x => x != null && x.StartsWith("attachment"))
137 ) {
138 // oops my bad gangalang
139 // ok now put it back
140 memoryStream.Position = 0;
141 await memoryStream.CopyToAsync(responseStream);
142 context.Response.Body = responseStream;
143
144 return;
145 }
146
147 memoryStream.Position = 0;
148 String html = await new StreamReader(memoryStream).ReadToEndAsync();
149 String minified = Utils.OptimizeHtml(html);
150
151 context.Response.ContentLength = Encoding.UTF8.GetByteCount(minified);
152 await responseStream.WriteAsync(Encoding.UTF8.GetBytes(minified));
153 context.Response.Body = responseStream;
154});
155
156app.UseRewriter(new RewriteOptions().AddRedirect(@"^favicon\.ico$", "/.nhnd/favicon.ico"));
157
158app.UseStaticFiles(new StaticFileOptions {
159 RequestPath = "/.nhnd"
160});
161
162// if the static handler didn't pick this up, then 404
163app.Use(async (context, next) => {
164 if (context.Request.Path.StartsWithSegments("/.nhnd")) {
165 context.Response.StatusCode = StatusCodes.Status404NotFound;
166 return;
167 }
168
169 await next(context);
170});
171
172app.MapRazorComponents<App>();
173
174app.Run();