初始化提交

This commit is contained in:
2026-02-03 16:52:44 +08:00
commit d2f9806384
512 changed files with 65167 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
#import <Foundation/Foundation.h>
#import "HTTPResponse.h"
// Wraps an HTTPResponse object to allow setting a custom status code
// without needing to create subclasses of every response.
@interface HTTPResponseProxy : NSObject <HTTPResponse>
@property (nonatomic) NSObject<HTTPResponse> *response;
@property (nonatomic) NSInteger status;
- (NSInteger)customStatus;
@end

View File

@@ -0,0 +1,84 @@
#import "HTTPResponseProxy.h"
#pragma clang diagnostic ignored "-Wdirect-ivar-access"
@implementation HTTPResponseProxy
@synthesize response;
@synthesize status;
- (NSInteger)status {
if (status != 0) {
return status;
} else if ([response respondsToSelector:@selector(status)]) {
return [response status];
}
return 200;
}
- (void)setStatus:(NSInteger)statusCode {
status = statusCode;
}
- (NSInteger)customStatus {
return status;
}
// Implement the required HTTPResponse methods
- (UInt64)contentLength {
if (response) {
return [response contentLength];
} else {
return 0;
}
}
- (UInt64)offset {
if (response) {
return [response offset];
} else {
return 0;
}
}
- (void)setOffset:(UInt64)offset {
if (response) {
[response setOffset:offset];
}
}
- (NSData *)readDataOfLength:(NSUInteger)length {
if (response) {
return [response readDataOfLength:length];
} else {
return nil;
}
}
- (BOOL)isDone {
if (response) {
return [response isDone];
} else {
return YES;
}
}
// Forward all other invocations to the actual response object
- (void)forwardInvocation:(NSInvocation *)invocation {
if ([response respondsToSelector:[invocation selector]]) {
[invocation invokeWithTarget:response];
} else {
[super forwardInvocation:invocation];
}
}
- (BOOL)respondsToSelector:(SEL)selector {
if ([super respondsToSelector:selector])
return YES;
return [response respondsToSelector:selector];
}
@end

View File

@@ -0,0 +1,19 @@
Copyright (c) 2011 Matt Stevens
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,18 @@
#import <Foundation/Foundation.h>
#import "RoutingHTTPServer.h"
@interface Route : NSObject
@property (nonatomic) NSRegularExpression *regex;
@property (nonatomic, copy) RequestHandler handler;
#if __has_feature(objc_arc_weak)
@property (nonatomic, weak) id target;
#else
@property (nonatomic, assign) id target;
#endif
@property (nonatomic, assign) SEL selector;
@property (nonatomic) NSArray *keys;
@end

View File

@@ -0,0 +1,11 @@
#import "Route.h"
@implementation Route
@synthesize regex;
@synthesize handler;
@synthesize target;
@synthesize selector;
@synthesize keys;
@end

View File

@@ -0,0 +1,16 @@
#import <Foundation/Foundation.h>
@class HTTPMessage;
@interface RouteRequest : NSObject
@property (nonatomic, readonly) NSDictionary *headers;
@property (nonatomic, readonly) NSDictionary *params;
- (id)initWithHTTPMessage:(HTTPMessage *)msg parameters:(NSDictionary *)params;
- (NSString *)header:(NSString *)field;
- (id)param:(NSString *)name;
- (NSString *)method;
- (NSURL *)url;
- (NSData *)body;
@end

View File

@@ -0,0 +1,50 @@
#import "RouteRequest.h"
#import "HTTPMessage.h"
#pragma clang diagnostic ignored "-Wdirect-ivar-access"
#pragma clang diagnostic ignored "-Widiomatic-parentheses"
@implementation RouteRequest {
HTTPMessage *message;
}
@synthesize params;
- (id)initWithHTTPMessage:(HTTPMessage *)msg parameters:(NSDictionary *)parameters {
if (self = [super init]) {
params = parameters;
message = msg;
}
return self;
}
- (NSDictionary *)headers {
return [message allHeaderFields];
}
- (NSString *)header:(NSString *)field {
return [message headerField:field];
}
- (id)param:(NSString *)name {
return [params objectForKey:name];
}
- (NSString *)method {
return [message method];
}
- (NSURL *)url {
return [message url];
}
- (NSData *)body {
return [message body];
}
- (NSString *)description {
NSData *data = [message messageData];
return [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
}
@end

View File

@@ -0,0 +1,20 @@
#import <Foundation/Foundation.h>
#import "HTTPResponse.h"
@class HTTPConnection;
@class HTTPResponseProxy;
@interface RouteResponse : NSObject
@property (nonatomic, unsafe_unretained, readonly) HTTPConnection *connection;
@property (nonatomic, readonly) NSDictionary *headers;
@property (nonatomic, strong) NSObject<HTTPResponse> *response;
@property (nonatomic, readonly) NSObject<HTTPResponse> *proxiedResponse;
@property (nonatomic) NSInteger statusCode;
- (id)initWithConnection:(HTTPConnection *)theConnection;
- (void)setHeader:(NSString *)field value:(NSString *)value;
- (void)respondWithString:(NSString *)string;
- (void)respondWithString:(NSString *)string encoding:(NSStringEncoding)encoding;
- (void)respondWithData:(NSData *)data;
@end

View File

@@ -0,0 +1,66 @@
#import "RouteResponse.h"
#import "HTTPConnection.h"
#import "HTTPDataResponse.h"
#import "HTTPResponseProxy.h"
#pragma clang diagnostic ignored "-Wdirect-ivar-access"
#pragma clang diagnostic ignored "-Widiomatic-parentheses"
@implementation RouteResponse {
NSMutableDictionary *headers;
HTTPResponseProxy *proxy;
}
@synthesize connection;
@synthesize headers;
- (id)initWithConnection:(HTTPConnection *)theConnection {
if (self = [super init]) {
connection = theConnection;
headers = [[NSMutableDictionary alloc] init];
proxy = [[HTTPResponseProxy alloc] init];
}
return self;
}
- (NSObject <HTTPResponse>*)response {
return proxy.response;
}
- (void)setResponse:(NSObject <HTTPResponse>*)response {
proxy.response = response;
}
- (NSObject <HTTPResponse>*)proxiedResponse {
if (proxy.response != nil || proxy.customStatus != 0 || [headers count] > 0) {
return proxy;
}
return nil;
}
- (NSInteger)statusCode {
return proxy.status;
}
- (void)setStatusCode:(NSInteger)status {
proxy.status = status;
}
- (void)setHeader:(NSString *)field value:(NSString *)value {
[headers setObject:value forKey:field];
}
- (void)respondWithString:(NSString *)string {
[self respondWithString:string encoding:NSUTF8StringEncoding];
}
- (void)respondWithString:(NSString *)string encoding:(NSStringEncoding)encoding {
[self respondWithData:[string dataUsingEncoding:encoding]];
}
- (void)respondWithData:(NSData *)data {
self.response = [[HTTPDataResponse alloc] initWithData:data];
}
@end

View File

@@ -0,0 +1,5 @@
#import <Foundation/Foundation.h>
#import "HTTPConnection.h"
@interface RoutingConnection : HTTPConnection
@end

View File

@@ -0,0 +1,142 @@
#import "RoutingConnection.h"
#import "RoutingHTTPServer.h"
#import "HTTPMessage.h"
#import "HTTPResponseProxy.h"
#pragma clang diagnostic ignored "-Wdirect-ivar-access"
#pragma clang diagnostic ignored "-Widiomatic-parentheses"
#pragma clang diagnostic ignored "-Wundeclared-selector"
@implementation RoutingConnection {
__unsafe_unretained RoutingHTTPServer *http;
NSDictionary *headers;
}
- (id)initWithAsyncSocket:(GCDAsyncSocket *)newSocket configuration:(HTTPConfig *)aConfig {
if (self = [super initWithAsyncSocket:newSocket configuration:aConfig]) {
NSAssert([config.server isKindOfClass:[RoutingHTTPServer class]],
@"A RoutingConnection is being used with a server that is not a %@",
NSStringFromClass([RoutingHTTPServer class]));
http = (RoutingHTTPServer *)config.server;
}
return self;
}
- (BOOL)supportsMethod:(NSString *)method atPath:(NSString *)path {
if ([http supportsMethod:method])
return YES;
return [super supportsMethod:method atPath:path];
}
- (BOOL)shouldHandleRequestForMethod:(NSString *)method atPath:(NSString *)path {
// The default implementation is strict about the use of Content-Length. Either
// a given method + path combination must *always* include data or *never*
// include data. The routing connection is lenient, a POST that sometimes does
// not include data or a GET that sometimes does is fine. It is up to the route
// implementations to decide how to handle these situations.
return YES;
}
- (void)processBodyData:(NSData *)postDataChunk {
BOOL result = [request appendData:postDataChunk];
if (!result) {
// TODO: Log
}
}
- (NSObject<HTTPResponse> *)httpResponseForMethod:(NSString *)method URI:(NSString *)path {
NSURL *url = [request url];
NSString *query = nil;
NSDictionary *params = [NSDictionary dictionary];
headers = nil;
if (url) {
path = [url path]; // Strip the query string from the path
query = [url query];
if (query) {
params = [self parseParams:query];
}
}
RouteResponse *response = [http routeMethod:method withPath:path parameters:params request:request connection:self];
if (response != nil) {
headers = response.headers;
return response.proxiedResponse;
}
// Set a MIME type for static files if possible
NSObject<HTTPResponse> *staticResponse = [super httpResponseForMethod:method URI:path];
if (staticResponse && [staticResponse respondsToSelector:@selector(filePath)]) {
NSString *mimeType = [http mimeTypeForPath:[staticResponse performSelector:@selector(filePath)]];
if (mimeType) {
headers = [NSDictionary dictionaryWithObject:mimeType forKey:@"Content-Type"];
}
}
return staticResponse;
}
- (void)responseHasAvailableData:(NSObject<HTTPResponse> *)sender {
HTTPResponseProxy *proxy = (HTTPResponseProxy *)httpResponse;
if (proxy.response == sender) {
[super responseHasAvailableData:httpResponse];
}
}
- (void)responseDidAbort:(NSObject<HTTPResponse> *)sender {
HTTPResponseProxy *proxy = (HTTPResponseProxy *)httpResponse;
if (proxy.response == sender) {
[super responseDidAbort:httpResponse];
}
}
- (void)setHeadersForResponse:(HTTPMessage *)response isError:(BOOL)isError {
[http.defaultHeaders enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL *stop) {
[response setHeaderField:field value:value];
}];
if (headers && !isError) {
[headers enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL *stop) {
[response setHeaderField:field value:value];
}];
}
// Set the connection header if not already specified
NSString *connection = [response headerField:@"Connection"];
if (!connection) {
connection = [self shouldDie] ? @"close" : @"keep-alive";
[response setHeaderField:@"Connection" value:connection];
}
}
- (NSData *)preprocessResponse:(HTTPMessage *)response {
[self setHeadersForResponse:response isError:NO];
return [super preprocessResponse:response];
}
- (NSData *)preprocessErrorResponse:(HTTPMessage *)response {
[self setHeadersForResponse:response isError:YES];
return [super preprocessErrorResponse:response];
}
- (BOOL)shouldDie {
__block BOOL shouldDie = [super shouldDie];
// Allow custom headers to determine if the connection should be closed
if (!shouldDie && headers) {
[headers enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL *stop) {
if ([field caseInsensitiveCompare:@"connection"] == NSOrderedSame) {
if ([value caseInsensitiveCompare:@"close"] == NSOrderedSame) {
shouldDie = YES;
}
*stop = YES;
}
}];
}
return shouldDie;
}
@end

View File

@@ -0,0 +1,55 @@
#import <Foundation/Foundation.h>
//! Project version number for Peertalk.
FOUNDATION_EXPORT double RoutingHTTPServerVersionNumber;
//! Project version string for Peertalk.
FOUNDATION_EXPORT const unsigned char RoutingHTTPServerVersionString[];
#import "HTTPServer.h"
#import "HTTPConnection.h"
#import "HTTPResponse.h"
#import "RouteResponse.h"
#import "RouteRequest.h"
#import "RoutingConnection.h"
#import "GCDAsyncSocket.h"
typedef void (^RequestHandler)(RouteRequest *request, RouteResponse *response);
@interface RoutingHTTPServer : HTTPServer
@property (nonatomic, readonly) NSDictionary *defaultHeaders;
// Specifies headers that will be set on every response.
// These headers can be overridden by RouteResponses.
- (void)setDefaultHeaders:(NSDictionary *)headers;
- (void)setDefaultHeader:(NSString *)field value:(NSString *)value;
// Returns the dispatch queue on which routes are processed.
// By default this is NULL and routes are processed on CocoaHTTPServer's
// connection queue. You can specify a queue to process routes on, such as
// dispatch_get_main_queue() to process all routes on the main thread.
- (dispatch_queue_t)routeQueue;
- (void)setRouteQueue:(dispatch_queue_t)queue;
- (NSDictionary *)mimeTypes;
- (void)setMIMETypes:(NSDictionary *)types;
- (void)setMIMEType:(NSString *)type forExtension:(NSString *)ext;
- (NSString *)mimeTypeForPath:(NSString *)path;
// Convenience methods. Yes I know, this is Cocoa and we don't use convenience
// methods because typing lengthy primitives over and over and over again is
// elegant with the beauty and the poetry. These are just, you know, here.
- (void)get:(NSString *)path withBlock:(RequestHandler)block;
- (void)post:(NSString *)path withBlock:(RequestHandler)block;
- (void)put:(NSString *)path withBlock:(RequestHandler)block;
- (void)delete:(NSString *)path withBlock:(RequestHandler)block;
- (void)handleMethod:(NSString *)method withPath:(NSString *)path block:(RequestHandler)block;
- (void)handleMethod:(NSString *)method withPath:(NSString *)path target:(id)target selector:(SEL)selector;
- (BOOL)supportsMethod:(NSString *)method;
- (RouteResponse *)routeMethod:(NSString *)method withPath:(NSString *)path parameters:(NSDictionary *)params request:(HTTPMessage *)request connection:(HTTPConnection *)connection;
@end

View File

@@ -0,0 +1,303 @@
#import "RoutingHTTPServer.h"
#import "RoutingConnection.h"
#import "Route.h"
#pragma clang diagnostic ignored "-Wdirect-ivar-access"
#pragma clang diagnostic ignored "-Widiomatic-parentheses"
@implementation RoutingHTTPServer {
NSMutableDictionary *routes;
NSMutableDictionary *defaultHeaders;
NSMutableDictionary *mimeTypes;
dispatch_queue_t routeQueue;
}
@synthesize defaultHeaders;
- (id)init {
if (self = [super init]) {
connectionClass = [RoutingConnection self];
routes = [[NSMutableDictionary alloc] init];
defaultHeaders = [[NSMutableDictionary alloc] init];
[self setupMIMETypes];
}
return self;
}
#if !OS_OBJECT_USE_OBJC_RETAIN_RELEASE
- (void)dealloc {
if (routeQueue)
dispatch_release(routeQueue);
}
#endif
- (void)setDefaultHeaders:(NSDictionary *)headers {
if (headers) {
defaultHeaders = [headers mutableCopy];
} else {
defaultHeaders = [[NSMutableDictionary alloc] init];
}
}
- (void)setDefaultHeader:(NSString *)field value:(NSString *)value {
[defaultHeaders setObject:value forKey:field];
}
- (dispatch_queue_t)routeQueue {
return routeQueue;
}
- (void)setRouteQueue:(dispatch_queue_t)queue {
#if !OS_OBJECT_USE_OBJC_RETAIN_RELEASE
if (queue)
dispatch_retain(queue);
if (routeQueue)
dispatch_release(routeQueue);
#endif
routeQueue = queue;
}
- (NSDictionary *)mimeTypes {
return mimeTypes;
}
- (void)setMIMETypes:(NSDictionary *)types {
NSMutableDictionary *newTypes;
if (types) {
newTypes = [types mutableCopy];
} else {
newTypes = [[NSMutableDictionary alloc] init];
}
mimeTypes = newTypes;
}
- (void)setMIMEType:(NSString *)theType forExtension:(NSString *)ext {
[mimeTypes setObject:theType forKey:ext];
}
- (NSString *)mimeTypeForPath:(NSString *)path {
NSString *ext = [[path pathExtension] lowercaseString];
if (!ext || [ext length] < 1)
return nil;
return [mimeTypes objectForKey:ext];
}
- (void)get:(NSString *)path withBlock:(RequestHandler)block {
[self handleMethod:@"GET" withPath:path block:block];
}
- (void)post:(NSString *)path withBlock:(RequestHandler)block {
[self handleMethod:@"POST" withPath:path block:block];
}
- (void)put:(NSString *)path withBlock:(RequestHandler)block {
[self handleMethod:@"PUT" withPath:path block:block];
}
- (void)delete:(NSString *)path withBlock:(RequestHandler)block {
[self handleMethod:@"DELETE" withPath:path block:block];
}
- (void)handleMethod:(NSString *)method
withPath:(NSString *)path
block:(RequestHandler)block {
Route *route = [self routeWithPath:path];
route.handler = block;
[self addRoute:route forMethod:method];
}
- (void)handleMethod:(NSString *)method
withPath:(NSString *)path
target:(id)target
selector:(SEL)selector {
Route *route = [self routeWithPath:path];
route.target = target;
route.selector = selector;
[self addRoute:route forMethod:method];
}
- (void)addRoute:(Route *)route forMethod:(NSString *)method {
method = [method uppercaseString];
NSMutableArray *methodRoutes = [routes objectForKey:method];
if (methodRoutes == nil) {
methodRoutes = [NSMutableArray array];
[routes setObject:methodRoutes forKey:method];
}
[methodRoutes addObject:route];
// Define a HEAD route for all GET routes
if ([method isEqualToString:@"GET"]) {
[self addRoute:route forMethod:@"HEAD"];
}
}
- (Route *)routeWithPath:(NSString *)path {
Route *route = [[Route alloc] init];
NSMutableArray *keys = [NSMutableArray array];
if ([path length] > 2 && [path characterAtIndex:0] == '{') {
// This is a custom regular expression, just remove the {}
path = [path substringWithRange:NSMakeRange(1, [path length] - 2)];
} else {
NSRegularExpression *regex = nil;
// Escape regex characters
regex = [NSRegularExpression regularExpressionWithPattern:@"[.+()]" options:0 error:nil];
path = [regex stringByReplacingMatchesInString:path options:0 range:NSMakeRange(0, path.length) withTemplate:@"\\\\$0"];
// Parse any :parameters and * in the path
regex = [NSRegularExpression regularExpressionWithPattern:@"(:(\\w+)|\\*)"
options:0
error:nil];
NSMutableString *regexPath = [NSMutableString stringWithString:path];
__block NSInteger diff = 0;
[regex enumerateMatchesInString:path options:0 range:NSMakeRange(0, path.length)
usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
NSRange replacementRange = NSMakeRange(diff + result.range.location, result.range.length);
NSString *replacementString;
NSString *capturedString = [path substringWithRange:result.range];
if ([capturedString isEqualToString:@"*"]) {
[keys addObject:@"wildcards"];
replacementString = @"(.*?)";
} else {
NSString *keyString = [path substringWithRange:[result rangeAtIndex:2]];
[keys addObject:keyString];
replacementString = @"([^/]+)";
}
[regexPath replaceCharactersInRange:replacementRange withString:replacementString];
diff += replacementString.length - result.range.length;
}];
path = [NSString stringWithFormat:@"^%@$", regexPath];
}
route.regex = [NSRegularExpression regularExpressionWithPattern:path options:NSRegularExpressionCaseInsensitive error:nil];
if ([keys count] > 0) {
route.keys = keys;
}
return route;
}
- (BOOL)supportsMethod:(NSString *)method {
return ([routes objectForKey:method] != nil);
}
- (void)handleRoute:(Route *)route
withRequest:(RouteRequest *)request
response:(RouteResponse *)response {
if (route.handler) {
route.handler(request, response);
} else {
id target = route.target;
SEL selector = route.selector;
NSMethodSignature *signature = [target methodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setSelector:selector];
[invocation setArgument:&request atIndex:2];
[invocation setArgument:&response atIndex:3];
[invocation invokeWithTarget:target];
}
}
- (RouteResponse *)routeMethod:(NSString *)method
withPath:(NSString *)path
parameters:(NSDictionary *)params
request:(HTTPMessage *)httpMessage
connection:(HTTPConnection *)connection {
NSMutableArray *methodRoutes = [routes objectForKey:method];
if (methodRoutes == nil)
return nil;
for (Route *route in methodRoutes) {
NSTextCheckingResult *result = [route.regex firstMatchInString:path options:0 range:NSMakeRange(0, path.length)];
if (!result)
continue;
// The first range is all of the text matched by the regex.
NSUInteger captureCount = [result numberOfRanges];
if (route.keys) {
// Add the route's parameters to the parameter dictionary, accounting for
// the first range containing the matched text.
if (captureCount == [route.keys count] + 1) {
NSMutableDictionary *newParams = [params mutableCopy];
NSUInteger index = 1;
BOOL firstWildcard = YES;
for (NSString *key in route.keys) {
NSString *capture = [path substringWithRange:[result rangeAtIndex:index]];
if ([key isEqualToString:@"wildcards"]) {
NSMutableArray *wildcards = [newParams objectForKey:key];
if (firstWildcard) {
// Create a new array and replace any existing object with the same key
wildcards = [NSMutableArray array];
[newParams setObject:wildcards forKey:key];
firstWildcard = NO;
}
[wildcards addObject:capture];
} else {
[newParams setObject:capture forKey:key];
}
index++;
}
params = newParams;
}
} else if (captureCount > 1) {
// For custom regular expressions place the anonymous captures in the captures parameter
NSMutableDictionary *newParams = [params mutableCopy];
NSMutableArray *captures = [NSMutableArray array];
for (NSUInteger i = 1; i < captureCount; i++) {
[captures addObject:[path substringWithRange:[result rangeAtIndex:i]]];
}
[newParams setObject:captures forKey:@"captures"];
params = newParams;
}
RouteRequest *request = [[RouteRequest alloc] initWithHTTPMessage:httpMessage parameters:params];
RouteResponse *response = [[RouteResponse alloc] initWithConnection:connection];
if (!routeQueue) {
[self handleRoute:route withRequest:request response:response];
} else {
// Process the route on the specified queue
dispatch_sync(routeQueue, ^{
@autoreleasepool {
[self handleRoute:route withRequest:request response:response];
}
});
}
return response;
}
return nil;
}
- (void)setupMIMETypes {
mimeTypes = [[NSMutableDictionary alloc] initWithObjectsAndKeys:
@"application/x-javascript", @"js",
@"image/gif", @"gif",
@"image/jpeg", @"jpg",
@"image/jpeg", @"jpeg",
@"image/png", @"png",
@"image/svg+xml", @"svg",
@"image/tiff", @"tif",
@"image/tiff", @"tiff",
@"image/x-icon", @"ico",
@"image/x-ms-bmp", @"bmp",
@"text/css", @"css",
@"text/html", @"html",
@"text/html", @"htm",
@"text/plain", @"txt",
@"text/xml", @"xml",
nil];
}
@end