aboutsummaryrefslogtreecommitdiff
path: root/Plugins.Essentials/src/Endpoints/ResourceEndpointBase.cs
blob: 4af3c304aa7bb2b5f8f5fc2d6cbdc261a3b42bc9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
/*
* Copyright (c) 2022 Vaughn Nugent
* 
* Library: VNLib
* Package: VNLib.Plugins.Essentials
* File: ResourceEndpointBase.cs 
*
* ResourceEndpointBase.cs is part of VNLib.Plugins.Essentials which is part of the larger 
* VNLib collection of libraries and utilities.
*
* VNLib.Plugins.Essentials is free software: you can redistribute it and/or modify 
* it under the terms of the GNU Affero General Public License as 
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* VNLib.Plugins.Essentials is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program.  If not, see https://www.gnu.org/licenses/.
*/

using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;

using VNLib.Net.Http;
using VNLib.Utils;
using VNLib.Utils.Logging;
using VNLib.Plugins.Essentials.Extensions;

namespace VNLib.Plugins.Essentials.Endpoints
{

    /// <summary>
    /// Provides a base class for implementing un-authenticated resource endpoints
    /// with basic (configurable) security checks
    /// </summary>
    public abstract class ResourceEndpointBase : VirtualEndpoint<HttpEntity>
    {
        /// <summary>
        /// Default protection settings. Protection settings are the most 
        /// secure by default, should be loosened an necessary
        /// </summary>
        protected virtual ProtectionSettings EndpointProtectionSettings { get; }

        ///<inheritdoc/>
        public override async ValueTask<VfReturnType> Process(HttpEntity entity)
        {
            try
            {
                ERRNO preProc = PreProccess(entity);
                if (preProc == ERRNO.E_FAIL)
                {
                    return VfReturnType.Forbidden;
                }
                //Entity was responded to by the pre-processor
                if (preProc < 0)
                {
                    return VfReturnType.VirtualSkip;
                }
                //If websockets are quested allow them to be processed in a logged-in/secure context
                if (entity.Server.IsWebSocketRequest)
                {
                    return await WebsocketRequestedAsync(entity);
                }
                ValueTask<VfReturnType> op = entity.Server.Method switch
                {
                    //Get request to get account
                    HttpMethod.GET => GetAsync(entity),
                    HttpMethod.POST => PostAsync(entity),
                    HttpMethod.DELETE => DeleteAsync(entity),
                    HttpMethod.PUT => PutAsync(entity),
                    HttpMethod.PATCH => PatchAsync(entity),
                    HttpMethod.OPTIONS => OptionsAsync(entity),
                    _ => AlternateMethodAsync(entity, entity.Server.Method),
                };
                return await op;
            }
            catch (InvalidJsonRequestException ije)
            {
                //Write the je to debug log
                Log.Debug(ije, "Failed to de-serialize a request entity body");
                //If the method is not POST/PUT/PATCH return a json message
                if ((entity.Server.Method & (HttpMethod.HEAD | HttpMethod.OPTIONS | HttpMethod.TRACE | HttpMethod.DELETE)) > 0)
                {
                    return VfReturnType.BadRequest;
                }
                //Only allow if json is an accepted response type
                if (!entity.Server.Accepts(ContentType.Json))
                {
                    return VfReturnType.BadRequest;
                }
                //Build web-message
                WebMessage webm = new()
                {
                    Result = "Request body is not valid json"
                };
                //Set the response webm
                entity.CloseResponseJson(HttpStatusCode.BadRequest, webm);
                //return virtual
                return VfReturnType.VirtualSkip;
            }
            catch (TerminateConnectionException)
            {
                //A TC exception is intentional and should always propagate to the runtime
                throw;
            }
            catch (ContentTypeUnacceptableException)
            {
                /*
                 * The runtime will handle a 406 unaccetptable response 
                 * and invoke the proper error app handler
                 */
                throw;
            }
            //Re-throw exceptions that are cause by reading the transport layer
            catch (IOException ioe) when (ioe.InnerException is SocketException)
            {
                throw;
            }
            catch (Exception ex)
            {
                //Log an uncaught excetpion and return an error code (log may not be initialized)
                Log?.Error(ex);
                return VfReturnType.Error;
            }
        }

        /// <summary>
        /// Allows for synchronous Pre-Processing of an entity. The result 
        /// will determine if the method processing methods will be invoked, or 
        /// a <see cref="VfReturnType.Forbidden"/> error code will be returned
        /// </summary>
        /// <param name="entity">The incomming request to process</param>
        /// <returns>
        /// True if processing should continue, false if the response should be 
        /// <see cref="VfReturnType.Forbidden"/>, less than 0 if entity was 
        /// responded to.
        /// </returns>
        protected virtual ERRNO PreProccess(HttpEntity entity)
        {
            //Disable cache if requested
            if (!EndpointProtectionSettings.EnableCaching)
            {
                entity.Server.SetNoCache();
            }

            //Enforce TLS
            if (!EndpointProtectionSettings.DisabledTlsRequired && !entity.IsSecure && !entity.IsLocalConnection)
            {
                return false;
            }

            //Enforce browser check
            if (!EndpointProtectionSettings.DisableBrowsersOnly && !entity.Server.IsBrowser())
            {
                return false;
            }

            //Enforce refer check
            if (!EndpointProtectionSettings.DisableRefererMatch && entity.Server.Referer != null && !entity.Server.RefererMatch())
            {
                return false;
            }

            //enforce session basic
            if (!EndpointProtectionSettings.DisableSessionsRequired && (!entity.Session.IsSet || entity.Session.IsNew))
            {
                return false;
            }

            /*
             * If sessions are required, verify cors is set, and the client supplied an origin header,
             * verify that it matches the origin that was specified during session initialization
             */
            if ((!EndpointProtectionSettings.DisableSessionsRequired & !EndpointProtectionSettings.DisableVerifySessionCors) && entity.Server.Origin != null && !entity.Session.CrossOriginMatch)
            {
                return false;
            }

            //Enforce cross-site
            if (!EndpointProtectionSettings.DisableCrossSiteDenied && entity.Server.IsCrossSite())
            {
                return false;
            }

            return true;
        }

        /// <summary>
        /// This method gets invoked when an incoming POST request to the endpoint has been requested.
        /// </summary>
        /// <param name="entity">The entity to be processed</param>
        /// <returns>The result of the operation to return to the file processor</returns>
        protected virtual ValueTask<VfReturnType> PostAsync(HttpEntity entity)
        {
            return ValueTask.FromResult(Post(entity));
        }
        /// <summary>
        /// This method gets invoked when an incoming GET request to the endpoint has been requested.
        /// </summary>
        /// <param name="entity">The entity to be processed</param>
        /// <returns>The result of the operation to return to the file processor</returns>
        protected virtual ValueTask<VfReturnType> GetAsync(HttpEntity entity)
        {
            return ValueTask.FromResult(Get(entity));
        }
        /// <summary>
        /// This method gets invoked when an incoming DELETE request to the endpoint has been requested.
        /// </summary>
        /// <param name="entity">The entity to be processed</param>
        /// <returns>The result of the operation to return to the file processor</returns>
        protected virtual ValueTask<VfReturnType> DeleteAsync(HttpEntity entity)
        {
            return ValueTask.FromResult(Delete(entity));
        }
        /// <summary>
        /// This method gets invoked when an incoming PUT request to the endpoint has been requested.
        /// </summary>
        /// <param name="entity">The entity to be processed</param>
        /// <returns>The result of the operation to return to the file processor</returns>
        protected virtual ValueTask<VfReturnType> PutAsync(HttpEntity entity)
        {
            return ValueTask.FromResult(Put(entity));
        }
        /// <summary>
        /// This method gets invoked when an incoming PATCH request to the endpoint has been requested.
        /// </summary>
        /// <param name="entity">The entity to be processed</param>
        /// <returns>The result of the operation to return to the file processor</returns>
        protected virtual ValueTask<VfReturnType> PatchAsync(HttpEntity entity)
        {
            return ValueTask.FromResult(Patch(entity));
        }

        protected virtual ValueTask<VfReturnType> OptionsAsync(HttpEntity entity)
        {
            return ValueTask.FromResult(Options(entity));
        }

        /// <summary>
        /// Invoked when a request is received for a method other than GET, POST, DELETE, or PUT;
        /// </summary>
        /// <param name="entity">The entity that </param>
        /// <param name="method">The request method</param>
        /// <returns>The results of the processing</returns>
        protected virtual ValueTask<VfReturnType> AlternateMethodAsync(HttpEntity entity, HttpMethod method)
        {
            return ValueTask.FromResult(AlternateMethod(entity, method));
        }

        /// <summary>
        /// Invoked when the current endpoint received a websocket request
        /// </summary>
        /// <param name="entity">The entity that requested the websocket</param>
        /// <returns>The results of the operation</returns>
        protected virtual ValueTask<VfReturnType> WebsocketRequestedAsync(HttpEntity entity)
        {
            return ValueTask.FromResult(WebsocketRequested(entity));
        }

        /// <summary>
        /// This method gets invoked when an incoming POST request to the endpoint has been requested.
        /// </summary>
        /// <param name="entity">The entity to be processed</param>
        /// <returns>The result of the operation to return to the file processor</returns>
        protected virtual VfReturnType Post(HttpEntity entity)
        {
            //Return method not allowed
            entity.CloseResponse(HttpStatusCode.MethodNotAllowed);
            return VfReturnType.VirtualSkip;
        }
        /// <summary>
        /// This method gets invoked when an incoming GET request to the endpoint has been requested.
        /// </summary>
        /// <param name="entity">The entity to be processed</param>
        /// <returns>The result of the operation to return to the file processor</returns>
        protected virtual VfReturnType Get(HttpEntity entity)
        {
            return VfReturnType.ProcessAsFile;
        }
        /// <summary>
        /// This method gets invoked when an incoming DELETE request to the endpoint has been requested.
        /// </summary>
        /// <param name="entity">The entity to be processed</param>
        /// <returns>The result of the operation to return to the file processor</returns>
        protected virtual VfReturnType Delete(HttpEntity entity)
        {
            entity.CloseResponse(HttpStatusCode.MethodNotAllowed);
            return VfReturnType.VirtualSkip;
        }
        /// <summary>
        /// This method gets invoked when an incoming PUT request to the endpoint has been requested.
        /// </summary>
        /// <param name="entity">The entity to be processed</param>
        /// <returns>The result of the operation to return to the file processor</returns>
        protected virtual VfReturnType Put(HttpEntity entity)
        {
            entity.CloseResponse(HttpStatusCode.MethodNotAllowed);
            return VfReturnType.VirtualSkip;
        }
        /// <summary>
        /// This method gets invoked when an incoming PATCH request to the endpoint has been requested.
        /// </summary>
        /// <param name="entity">The entity to be processed</param>
        /// <returns>The result of the operation to return to the file processor</returns>
        protected virtual VfReturnType Patch(HttpEntity entity)
        {
            entity.CloseResponse(HttpStatusCode.MethodNotAllowed);
            return VfReturnType.VirtualSkip;
        }
        /// <summary>
        /// Invoked when a request is received for a method other than GET, POST, DELETE, or PUT;
        /// </summary>
        /// <param name="entity">The entity that </param>
        /// <param name="method">The request method</param>
        /// <returns>The results of the processing</returns>
        protected virtual VfReturnType AlternateMethod(HttpEntity entity, HttpMethod method)
        {
            //Return method not allowed
            entity.CloseResponse(HttpStatusCode.MethodNotAllowed);
            return VfReturnType.VirtualSkip;
        }

        protected virtual VfReturnType Options(HttpEntity entity)
        {
            return VfReturnType.Forbidden;
        }

        /// <summary>
        /// Invoked when the current endpoint received a websocket request
        /// </summary>
        /// <param name="entity">The entity that requested the websocket</param>
        /// <returns>The results of the operation</returns>
        protected virtual VfReturnType WebsocketRequested(HttpEntity entity)
        {
            entity.CloseResponse(HttpStatusCode.Forbidden);
            return VfReturnType.VirtualSkip;
        }
    }
}