diff options
-rw-r--r-- | Makefile | 10 | ||||
-rw-r--r-- | src/client/libmumblelink.c | 63 | ||||
-rw-r--r-- | src/client/libmumblelink.h | 9 | ||||
-rw-r--r-- | src/qcommon/common.c | 6 | ||||
-rw-r--r-- | src/qcommon/cvar.c | 18 | ||||
-rw-r--r-- | src/renderer/tr_init.c | 4 | ||||
-rw-r--r-- | src/renderer/tr_local.h | 1 | ||||
-rw-r--r-- | src/renderer/tr_scene.c | 8 | ||||
-rw-r--r-- | src/server/sv_main.c | 217 | ||||
-rw-r--r-- | src/sys/con_tty.c | 7 | ||||
-rw-r--r-- | src/sys/sys_main.c | 1 | ||||
-rw-r--r-- | src/sys/sys_unix.c | 40 |
12 files changed, 334 insertions, 50 deletions
@@ -192,7 +192,6 @@ ifneq ($(BUILD_CLIENT),0) CURL_LIBS=$(shell pkg-config --silence-errors --libs libcurl) OPENAL_CFLAGS=$(shell pkg-config --silence-errors --cflags openal) OPENAL_LIBS=$(shell pkg-config --silence-errors --libs openal) - # FIXME: introduce CLIENT_CFLAGS SDL_CFLAGS=$(shell pkg-config --silence-errors --cflags sdl|sed 's/-Dmain=SDL_main//') SDL_LIBS=$(shell pkg-config --silence-errors --libs sdl) endif @@ -556,7 +555,7 @@ ifeq ($(PLATFORM),freebsd) CLIENT_CFLAGS += -DUSE_CODEC_VORBIS endif - OPTIMIZEVM = -DNDEBUG -O3 -funroll-loops -fomit-frame-pointer + OPTIMIZEVM = -O3 -funroll-loops -fomit-frame-pointer ifeq ($(ARCH),axp) BASE_CFLAGS += -DNO_VM_COMPILED @@ -745,7 +744,7 @@ ifeq ($(PLATFORM),sunos) CLIENT_CFLAGS = $(SDL_CFLAGS) SERVER_CFLAGS = - OPTIMIZEVM = -O3 -funroll-loops -DNDEBUG + OPTIMIZEVM = -O3 -funroll-loops ifeq ($(ARCH),sparc) OPTIMIZEVM += -O3 \ @@ -787,7 +786,7 @@ else # ifeq sunos # SETUP AND BUILD -- GENERIC ############################################################################# BASE_CFLAGS=-DNO_VM_COMPILED - OPTIMIZE = -DNDEBUG -O3 + OPTIMIZE = -O3 SHLIBEXT=so SHLIBCFLAGS=-fPIC @@ -845,6 +844,7 @@ endif ifeq ($(USE_VOIP),1) CLIENT_CFLAGS += -DUSE_VOIP + SERVER_CFLAGS += -DUSE_VOIP ifeq ($(USE_INTERNAL_SPEEX),1) CLIENT_CFLAGS += -DFLOATING_POINT -DUSE_ALLOCA -I$(SPEEXDIR)/include else @@ -966,7 +966,7 @@ endif release: @$(MAKE) targets B=$(BR) CFLAGS="$(CFLAGS) $(BASE_CFLAGS) $(DEPEND_CFLAGS)" \ - OPTIMIZE="$(OPTIMIZE)" OPTIMIZEVM="$(OPTIMIZEVM)" \ + OPTIMIZE="-DNDEBUG $(OPTIMIZE)" OPTIMIZEVM="-DNDEBUG $(OPTIMIZEVM)" \ CLIENT_CFLAGS="$(CLIENT_CFLAGS)" SERVER_CFLAGS="$(SERVER_CFLAGS)" V=$(V) ifeq ($(BUILD_MASTER_SERVER),1) $(MAKE) -C $(MASTERDIR) release diff --git a/src/client/libmumblelink.c b/src/client/libmumblelink.c index 07b0c1ea..1b1e521e 100644 --- a/src/client/libmumblelink.c +++ b/src/client/libmumblelink.c @@ -40,14 +40,26 @@ #include "libmumblelink.h" +#ifndef MIN +#define MIN(a, b) ((a)<(b)?(a):(b)) +#endif + typedef struct { uint32_t uiVersion; uint32_t uiTick; - float fPosition[3]; - float fFront[3]; - float fTop[3]; + float fAvatarPosition[3]; + float fAvatarFront[3]; + float fAvatarTop[3]; wchar_t name[256]; + /* new in mumble 1.2 */ + float fCameraPosition[3]; + float fCameraFront[3]; + float fCameraTop[3]; + wchar_t identity[256]; + uint32_t context_len; + unsigned char context[256]; + wchar_t description[2048]; } LinkedMem; static LinkedMem *lm = NULL; @@ -95,6 +107,8 @@ int mumble_link(const char* name) lm = (LinkedMem *) (mmap(NULL, sizeof(LinkedMem), PROT_READ | PROT_WRITE, MAP_SHARED, shmfd,0)); if (lm == (void *) (-1)) { lm = NULL; + close(shmfd); + return -1; } close(shmfd); #endif @@ -105,16 +119,51 @@ int mumble_link(const char* name) void mumble_update_coordinates(float fPosition[3], float fFront[3], float fTop[3]) { + mumble_update_coordinates2(fPosition, fFront, fTop, fPosition, fFront, fTop); +} + +void mumble_update_coordinates2(float fAvatarPosition[3], float fAvatarFront[3], float fAvatarTop[3], + float fCameraPosition[3], float fCameraFront[3], float fCameraTop[3]) +{ if (!lm) return; - memcpy(lm->fPosition, fPosition, sizeof(fPosition)); - memcpy(lm->fFront, fFront, sizeof(fFront)); - memcpy(lm->fTop, fTop, sizeof(fTop)); - lm->uiVersion = 1; + memcpy(lm->fAvatarPosition, fAvatarPosition, sizeof(fAvatarPosition)); + memcpy(lm->fAvatarFront, fAvatarFront, sizeof(fAvatarFront)); + memcpy(lm->fAvatarTop, fAvatarTop, sizeof(fAvatarTop)); + memcpy(lm->fCameraPosition, fCameraPosition, sizeof(fCameraPosition)); + memcpy(lm->fCameraFront, fCameraFront, sizeof(fCameraFront)); + memcpy(lm->fCameraTop, fCameraTop, sizeof(fCameraTop)); + lm->uiVersion = 2; lm->uiTick = GetTickCount(); } +void mumble_set_identity(const char* identity) +{ + size_t len; + if (!lm) + return; + len = MIN(sizeof(lm->identity), strlen(identity)+1); + mbstowcs(lm->identity, identity, len); +} + +void mumble_set_context(const unsigned char* context, size_t len) +{ + if (!lm) + return; + len = MIN(sizeof(lm->context), len); + memcpy(lm->context, context, len); +} + +void mumble_set_description(const char* description) +{ + size_t len; + if (!lm) + return; + len = MIN(sizeof(lm->description), strlen(description)+1); + mbstowcs(lm->description, description, len); +} + void mumble_unlink() { if(!lm) diff --git a/src/client/libmumblelink.h b/src/client/libmumblelink.h index 805b9850..fc4929ff 100644 --- a/src/client/libmumblelink.h +++ b/src/client/libmumblelink.h @@ -24,4 +24,13 @@ int mumble_link(const char* name); int mumble_islinked(void); void mumble_update_coordinates(float fPosition[3], float fFront[3], float fTop[3]); + +/* new for mumble 1.2: also set camera position */ +void mumble_update_coordinates2(float fAvatarPosition[3], float fAvatarFront[3], float fAvatarTop[3], + float fCameraPosition[3], float fCameraFront[3], float fCameraTop[3]); + +void mumble_set_description(const char* description); +void mumble_set_context(const unsigned char* context, size_t len); +void mumble_set_identity(const char* identity); + void mumble_unlink(void); diff --git a/src/qcommon/common.c b/src/qcommon/common.c index eb3eca1b..d9fb4495 100644 --- a/src/qcommon/common.c +++ b/src/qcommon/common.c @@ -2919,6 +2919,12 @@ void Com_Frame( void ) { if ( com_speeds->integer ) { timeAfter = Sys_Milliseconds (); } +#else + if ( com_speeds->integer ) { + timeAfter = Sys_Milliseconds (); + timeBeforeEvents = timeAfter; + timeBeforeClient = timeAfter; + } #endif // diff --git a/src/qcommon/cvar.c b/src/qcommon/cvar.c index 6358bf04..b0a21506 100644 --- a/src/qcommon/cvar.c +++ b/src/qcommon/cvar.c @@ -383,12 +383,12 @@ cvar_t *Cvar_Get( const char *var_name, const char *var_value, int flags ) { var->latchedString = NULL; // otherwise cvar_set2 would free it Cvar_Set2( var_name, s, qtrue ); Z_Free( s ); - - // ZOID--needs to be set so that cvars the game sets as - // SERVERINFO get sent to clients - cvar_modifiedFlags |= flags; } + // ZOID--needs to be set so that cvars the game sets as + // SERVERINFO get sent to clients + cvar_modifiedFlags |= flags; + return var; } @@ -1165,6 +1165,16 @@ void Cvar_Register(vmCvar_t *vmCvar, const char *varName, const char *defaultVal { cvar_t *cv; + // There is code in Cvar_Get to prevent CVAR_ROM cvars being changed by the + // user. In other words CVAR_ARCHIVE and CVAR_ROM are mutually exclusive + // flags. Unfortunately some historical game code (including single player + // baseq3) sets both flags. We unset CVAR_ROM for such cvars. + if ((flags & (CVAR_ARCHIVE | CVAR_ROM)) == (CVAR_ARCHIVE | CVAR_ROM)) { + Com_DPrintf( S_COLOR_YELLOW "WARNING: Unsetting CVAR_ROM cvar '%s', " + "since it is also CVAR_ARCHIVE\n", varName ); + flags &= ~CVAR_ROM; + } + cv = Cvar_Get(varName, defaultValue, flags | CVAR_VM_CREATED); if (!vmCvar) diff --git a/src/renderer/tr_init.c b/src/renderer/tr_init.c index 90b74683..89fcf3e3 100644 --- a/src/renderer/tr_init.c +++ b/src/renderer/tr_init.c @@ -42,8 +42,6 @@ cvar_t *r_ignoreFastPath; cvar_t *r_verbose; cvar_t *r_ignore; -cvar_t *r_displayRefresh; - cvar_t *r_detailTextures; cvar_t *r_znear; @@ -938,8 +936,6 @@ void R_Register( void ) // // temporary latched variables that can only change over a restart // - r_displayRefresh = ri.Cvar_Get( "r_displayRefresh", "0", CVAR_LATCH ); - ri.Cvar_CheckRange( r_displayRefresh, 0, 200, qtrue ); r_fullbright = ri.Cvar_Get ("r_fullbright", "0", CVAR_LATCH|CVAR_CHEAT ); r_mapOverBrightBits = ri.Cvar_Get ("r_mapOverBrightBits", "2", CVAR_LATCH ); r_intensity = ri.Cvar_Get ("r_intensity", "1", CVAR_LATCH ); diff --git a/src/renderer/tr_local.h b/src/renderer/tr_local.h index 231855e9..ad32c260 100644 --- a/src/renderer/tr_local.h +++ b/src/renderer/tr_local.h @@ -1044,7 +1044,6 @@ extern cvar_t *r_pixelAspect; extern cvar_t *r_fullscreen; extern cvar_t *r_noborder; extern cvar_t *r_gamma; -extern cvar_t *r_displayRefresh; // optional display refresh option extern cvar_t *r_ignorehwgamma; // overrides hardware gamma capabilities extern cvar_t *r_allowExtensions; // global enable/disable of OpenGL extensions diff --git a/src/renderer/tr_scene.c b/src/renderer/tr_scene.c index ae1376e9..232cb684 100644 --- a/src/renderer/tr_scene.c +++ b/src/renderer/tr_scene.c @@ -213,10 +213,10 @@ void RE_AddRefEntityToScene( const refEntity_t *ent ) { return; } if ( Q_isnan(ent->origin[0]) || Q_isnan(ent->origin[1]) || Q_isnan(ent->origin[2]) ) { - static qboolean first_time = qtrue; - if (first_time) { - first_time = qfalse; - Com_Printf(S_COLOR_YELLOW "WARNING: You might have built ioquake3 with a buggy compiler!\n"); + static qboolean firstTime = qtrue; + if (firstTime) { + firstTime = qfalse; + Com_DPrintf(S_COLOR_YELLOW "WARNING: RE_AddRefEntityToScene passed a refEntity which has an origin with a NaN component\n"); } return; } diff --git a/src/server/sv_main.c b/src/server/sv_main.c index 75e9dc72..4363e532 100644 --- a/src/server/sv_main.c +++ b/src/server/sv_main.c @@ -370,6 +370,182 @@ CONNECTIONLESS COMMANDS ============================================================================== */ +typedef struct leakyBucket_s leakyBucket_t; +struct leakyBucket_s { + netadrtype_t type; + + union { + byte _4[4]; + byte _6[16]; + } ipv; + + int lastTime; + signed char burst; + + long hash; + + leakyBucket_t *prev, *next; +}; + +// This is deliberately quite large to make it more of an effort to DoS +#define MAX_BUCKETS 16384 +#define MAX_HASHES 1024 + +static leakyBucket_t buckets[ MAX_BUCKETS ]; +static leakyBucket_t *bucketHashes[ MAX_HASHES ]; + +/* +================ +SVC_HashForAddress +================ +*/ +static long SVC_HashForAddress( netadr_t address ) { + byte *ip = NULL; + size_t size = 0; + int i; + long hash = 0; + + switch ( address.type ) { + case NA_IP: ip = address.ip; size = 4; break; + case NA_IP6: ip = address.ip6; size = 16; break; + default: break; + } + + for ( i = 0; i < size; i++ ) { + hash += (long)( ip[ i ] ) * ( i + 119 ); + } + + hash = ( hash ^ ( hash >> 10 ) ^ ( hash >> 20 ) ); + hash &= ( MAX_HASHES - 1 ); + + return hash; +} + +/* +================ +SVC_BucketForAddress + +Find or allocate a bucket for an address +================ +*/ +static leakyBucket_t *SVC_BucketForAddress( netadr_t address, int burst, int period ) { + leakyBucket_t *bucket = NULL; + int i; + long hash = SVC_HashForAddress( address ); + int now = Sys_Milliseconds(); + + for ( bucket = bucketHashes[ hash ]; bucket; bucket = bucket->next ) { + switch ( bucket->type ) { + case NA_IP: + if ( memcmp( bucket->ipv._4, address.ip, 4 ) == 0 ) { + return bucket; + } + break; + + case NA_IP6: + if ( memcmp( bucket->ipv._6, address.ip6, 16 ) == 0 ) { + return bucket; + } + break; + + default: + break; + } + } + + for ( i = 0; i < MAX_BUCKETS; i++ ) { + int interval; + + bucket = &buckets[ i ]; + interval = now - bucket->lastTime; + + // Reclaim expired buckets + if ( bucket->lastTime > 0 && interval > ( burst * period ) ) { + if ( bucket->prev != NULL ) { + bucket->prev->next = bucket->next; + } else { + bucketHashes[ bucket->hash ] = bucket->next; + } + + if ( bucket->next != NULL ) { + bucket->next->prev = bucket->prev; + } + + Com_Memset( bucket, 0, sizeof( leakyBucket_t ) ); + } + + if ( bucket->type == NA_BAD ) { + bucket->type = address.type; + switch ( address.type ) { + case NA_IP: Com_Memcpy( bucket->ipv._4, address.ip, 4 ); break; + case NA_IP6: Com_Memcpy( bucket->ipv._6, address.ip6, 16 ); break; + default: break; + } + + bucket->lastTime = now; + bucket->burst = 0; + bucket->hash = hash; + + // Add to the head of the relevant hash chain + bucket->next = bucketHashes[ hash ]; + if ( bucketHashes[ hash ] != NULL ) { + bucketHashes[ hash ]->prev = bucket; + } + + bucket->prev = NULL; + bucketHashes[ hash ] = bucket; + + return bucket; + } + } + + // Couldn't allocate a bucket for this address + return NULL; +} + +/* +================ +SVC_RateLimit +================ +*/ +static qboolean SVC_RateLimit( leakyBucket_t *bucket, int burst, int period ) { + if ( bucket != NULL ) { + int now = Sys_Milliseconds(); + int interval = now - bucket->lastTime; + int expired = interval / period; + int expiredRemainder = interval % period; + + if ( expired > bucket->burst ) { + bucket->burst = 0; + bucket->lastTime = now; + } else { + bucket->burst -= expired; + bucket->lastTime = now - expiredRemainder; + } + + if ( bucket->burst < burst ) { + bucket->burst++; + + return qfalse; + } + } + + return qtrue; +} + +/* +================ +SVC_RateLimitAddress + +Rate limit for a particular address +================ +*/ +static qboolean SVC_RateLimitAddress( netadr_t from, int burst, int period ) { + leakyBucket_t *bucket = SVC_BucketForAddress( from, burst, period ); + + return SVC_RateLimit( bucket, burst, period ); +} + /* ================ SVC_Status @@ -388,6 +564,21 @@ static void SVC_Status( netadr_t from ) { int statusLength; int playerLength; char infostring[MAX_INFO_STRING]; + static leakyBucket_t bucket; + + // Prevent using getstatus as an amplifier + if ( SVC_RateLimitAddress( from, 10, 1000 ) ) { + Com_DPrintf( "SVC_Status: rate limit from %s exceeded, dropping request\n", + NET_AdrToString( from ) ); + return; + } + + // Allow getstatus to be DoSed relatively easily, but prevent + // excess outbound bandwidth usage when being flooded inbound + if ( SVC_RateLimit( &bucket, 10, 100 ) ) { + Com_DPrintf( "SVC_Status: rate limit exceeded, dropping request\n" ); + return; + } strcpy( infostring, Cvar_InfoString( CVAR_SERVERINFO ) ); @@ -501,24 +692,30 @@ Redirect all printfs */ static void SVC_RemoteCommand( netadr_t from, msg_t *msg ) { qboolean valid; - unsigned int time; char remaining[1024]; // TTimo - scaled down to accumulate, but not overflow anything network wise, print wise etc. // (OOB messages are the bottleneck here) #define SV_OUTPUTBUF_LENGTH (1024 - 16) char sv_outputbuf[SV_OUTPUTBUF_LENGTH]; - static unsigned int lasttime = 0; char *cmd_aux; - // TTimo - https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=534 - time = Com_Milliseconds(); - if ( (unsigned)( time - lasttime ) < 500u ) { + // Prevent using rcon as an amplifier and make dictionary attacks impractical + if ( SVC_RateLimitAddress( from, 10, 1000 ) ) { + Com_DPrintf( "SVC_Status: rate limit from %s exceeded, dropping request\n", + NET_AdrToString( from ) ); return; } - lasttime = time; if ( !strlen( sv_rconPassword->string ) || strcmp (Cmd_Argv(1), sv_rconPassword->string) ) { + static leakyBucket_t bucket; + + // Make DoS via rcon impractical + if ( SVC_RateLimit( &bucket, 10, 1000 ) ) { + Com_DPrintf( "SVC_Status: rate limit exceeded, dropping request\n" ); + return; + } + valid = qfalse; Com_Printf ("Bad rcon from %s: %s\n", NET_AdrToString (from), Cmd_ArgsFrom(2) ); } else { @@ -587,7 +784,7 @@ static void SV_ConnectionlessPacket( netadr_t from, msg_t *msg ) { Com_DPrintf ("SV packet %s : %s\n", NET_AdrToString(from), c); if (!Q_stricmp(c, "getstatus")) { - SVC_Status( from ); + SVC_Status( from ); } else if (!Q_stricmp(c, "getinfo")) { SVC_Info( from ); } else if (!Q_stricmp(c, "getchallenge")) { @@ -601,8 +798,8 @@ static void SV_ConnectionlessPacket( netadr_t from, msg_t *msg ) { // server disconnect messages when their new server sees our final // sequenced messages to the old client } else { - Com_DPrintf ("bad connectionless packet from %s:\n%s\n" - , NET_AdrToString (from), s); + Com_DPrintf ("bad connectionless packet from %s:\n%s\n", + NET_AdrToString (from), s); } } @@ -610,7 +807,7 @@ static void SV_ConnectionlessPacket( netadr_t from, msg_t *msg ) { /* ================= -SV_ReadPackets +SV_PacketEvent ================= */ void SV_PacketEvent( netadr_t from, msg_t *msg ) { diff --git a/src/sys/con_tty.c b/src/sys/con_tty.c index 547521c7..e4b2ad5c 100644 --- a/src/sys/con_tty.c +++ b/src/sys/con_tty.c @@ -42,6 +42,7 @@ called before and after a stdout or stderr output ============================================================= */ +extern qboolean stdinIsATTY; static qboolean stdin_active; // general flag to tell about tty console mode static qboolean ttycon_on = qfalse; @@ -270,21 +271,19 @@ Initialize the console input (tty mode if possible) void CON_Init( void ) { struct termios tc; - const char* term = getenv("TERM"); // If the process is backgrounded (running non interactively) // then SIGTTIN or SIGTOU is emitted, if not caught, turns into a SIGSTP signal(SIGTTIN, SIG_IGN); signal(SIGTTOU, SIG_IGN); - + // If SIGCONT is received, reinitialize console signal(SIGCONT, CON_SigCont); // Make stdin reads non-blocking fcntl(STDIN_FILENO, F_SETFL, fcntl(STDIN_FILENO, F_GETFL, 0) | O_NONBLOCK ); - if (isatty(STDIN_FILENO) != 1 - || (term && (!strcmp(term, "raw") || !strcmp(term, "dumb")))) + if (!stdinIsATTY) { Com_Printf("tty console mode disabled\n"); ttycon_on = qfalse; diff --git a/src/sys/sys_main.c b/src/sys/sys_main.c index 9fadfa06..f76cb014 100644 --- a/src/sys/sys_main.c +++ b/src/sys/sys_main.c @@ -558,6 +558,7 @@ int main( int argc, char **argv ) signal( SIGFPE, Sys_SigHandler ); signal( SIGSEGV, Sys_SigHandler ); signal( SIGTERM, Sys_SigHandler ); + signal( SIGINT, Sys_SigHandler ); while( 1 ) { diff --git a/src/sys/sys_unix.c b/src/sys/sys_unix.c index 7863c082..ccff6312 100644 --- a/src/sys/sys_unix.c +++ b/src/sys/sys_unix.c @@ -38,6 +38,8 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #include <libgen.h> #include <fcntl.h> +qboolean stdinIsATTY; + // Used to determine where to store user-specific files static char homePath[ MAX_OSPATH ] = { 0 }; @@ -355,7 +357,7 @@ char **Sys_ListFiles( const char *directory, const char *extension, char *filter } extLen = strlen( extension ); - + // search nfiles = 0; @@ -465,24 +467,35 @@ Block execution for msec or until input is recieved. */ void Sys_Sleep( int msec ) { - fd_set fdset; - if( msec == 0 ) return; - FD_ZERO(&fdset); - FD_SET(fileno(stdin), &fdset); - if( msec < 0 ) + if( stdinIsATTY ) { - select((fileno(stdin) + 1), &fdset, NULL, NULL, NULL); + fd_set fdset; + + FD_ZERO(&fdset); + FD_SET(STDIN_FILENO, &fdset); + if( msec < 0 ) + { + select(STDIN_FILENO + 1, &fdset, NULL, NULL, NULL); + } + else + { + struct timeval timeout; + + timeout.tv_sec = msec/1000; + timeout.tv_usec = (msec%1000)*1000; + select(STDIN_FILENO + 1, &fdset, NULL, NULL, &timeout); + } } else { - struct timeval timeout; + // With nothing to select() on, we can't wait indefinitely + if( msec < 0 ) + msec = 10; - timeout.tv_sec = msec/1000; - timeout.tv_usec = (msec%1000)*1000; - select((fileno(stdin) + 1), &fdset, NULL, NULL, &timeout); + usleep( msec * 1000 ); } } @@ -572,11 +585,16 @@ Unix specific initialisation */ void Sys_PlatformInit( void ) { + const char* term = getenv( "TERM" ); + signal( SIGHUP, Sys_SigHandler ); signal( SIGQUIT, Sys_SigHandler ); signal( SIGTRAP, Sys_SigHandler ); signal( SIGIOT, Sys_SigHandler ); signal( SIGBUS, Sys_SigHandler ); + + stdinIsATTY = isatty( STDIN_FILENO ) && + !( term && ( !strcmp( term, "raw" ) || !strcmp( term, "dumb" ) ) ); } /* |