Last active
July 18, 2020 22:41
-
-
Save TerrorBite/4948812 to your computer and use it in GitHub Desktop.
MCPing Minecraft ping utility
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <stdio.h> | |
#include <stdlib.h> | |
#include <unistd.h> | |
#include <string.h> | |
#include <sys/types.h> | |
#include <sys/socket.h> | |
#include <netinet/in.h> | |
#include <netdb.h> | |
#include <sys/time.h> | |
#include <iconv.h> | |
#include <errno.h> | |
#include <argp.h> | |
void error(const char* msg) { | |
perror(msg); | |
exit(0); | |
} | |
/* Begin ArgP stuff */ | |
/* Version string and maintainer */ | |
const char *argp_program_version = | |
"MCPing v1.0"; | |
const char *argp_program_bug_address = | |
"TerrorBite <terrorbite@lethargiclion.net>"; | |
/* Documentation string */ | |
static char doc[] = "Ping a Minecraft server via the Minecraft protocol\n\n\ | |
HOST is the hostname or IP address to ping. By default, 5 packets are sent to port 25565."; | |
/* List of mandatory arguments, space-separated. Used for documentation */ | |
static char args_doc[] = "HOST"; | |
/* For ease of alteration, we have this */ | |
#define MCPING_ARG_COUNT 1 | |
/* List of optional arguments, with descriptions */ | |
static struct argp_option options[] = { | |
{"count", 'c', "COUNT", 0, "Send COUNT pings to the server"}, | |
{"port", 'p', "PORT", 0, "Use an alternate port instead of 25565"}, | |
{0} | |
}; | |
/* Data structure to hold the parsed arguments passed into the program */ | |
struct arguments { | |
/* We have only one non-named (mandatory) argument. | |
* This code can be expanded to support more than one */ | |
char *args[MCPING_ARG_COUNT]; | |
/* Optional arguments */ | |
long count; | |
uint16_t port; | |
}; | |
static error_t parse_opt(int key, char* arg, struct argp_state* state) { | |
/* Get the input argument from argp_parse, which we | |
know is a pointer to our arguments structure. */ | |
struct arguments* arguments = state->input; | |
long temp; | |
switch(key) { | |
case 'c': | |
temp = strtol(arg, (char**)NULL, 10); | |
if(errno) error("Error parsing COUNT"); | |
if(temp<=0) {fprintf(stderr, "COUNT must be greater than 0\n"); exit(1); } | |
arguments->count = temp; | |
break; | |
case 'p': | |
temp = strtol(arg, (char**)NULL, 10); | |
if(errno) error("Error parsing COUNT"); | |
if(temp<0||temp>65535) {fprintf(stderr, "PORT must be between 0 and 65535\n"); exit(1); } | |
arguments->port = (uint16_t)(temp & 0xFFFF); | |
break; | |
case ARGP_KEY_ARG: | |
/* Reached when parsing a non-named (mandatory) argument */ | |
if(state->arg_num >= MCPING_ARG_COUNT) /* Too many arguments */ | |
argp_usage(state); | |
arguments->args[state->arg_num] = arg; | |
break; | |
case ARGP_KEY_END: | |
/* Reached when all non-named (mandatory) arguments have been parsed */ | |
if(state->arg_num < MCPING_ARG_COUNT) /* Not enough arguments were passed */ | |
argp_usage(state); | |
break; | |
default: | |
return ARGP_ERR_UNKNOWN; | |
} | |
return 0; | |
} | |
/* This is our argp parser */ | |
static struct argp argp = { options, parse_opt, args_doc, doc }; | |
/* End ArgP stuff */ | |
typedef struct { | |
char* ptr; | |
int proto; | |
char* version; | |
char* motd; | |
int players; | |
int maxplayers; | |
} mc_resp; | |
char* nultok(char* src) { | |
static char* _src; | |
if(src!=NULL) _src = src; | |
while(*_src != '\0') ++_src; | |
++_src; | |
return _src; | |
} | |
mc_resp* decode(char* response, size_t length) { | |
/* Discard first three bytes (which are 0xFF = kick packet, then 2 bytes giving the length of the following data) */ | |
char* src = response+3; | |
size_t srclen = (length-3); | |
mc_resp* data; | |
/* Set up buffer for iconv, and get an extra pointer to it */ | |
char* buffer[256]; size_t buflen = 256; | |
char* bufptr = (char*)buffer; bzero(bufptr, buflen); | |
/* Convert UCS-2 multibyte data into UTF-8 */ | |
iconv_t conv = iconv_open("UTF-8", "UCS-2BE"); | |
iconv(conv, &src, &srclen, &bufptr, &buflen); | |
if(errno) error("Failed to decode server data"); | |
iconv_close(conv); | |
/* Compute the size of the converted string and allocate enough space to hold it */ | |
int len=257-buflen; | |
char* dest = malloc(len); | |
/* Copy buffer into allocated space */ | |
bzero(dest, len); | |
bcopy(buffer, dest, len); | |
/* Tokenize data into struct and return a pointer to it*/ | |
data = malloc(sizeof(mc_resp)); | |
data->ptr = dest; | |
data->proto = atoi(nultok(dest)); | |
data->version = nultok(NULL); | |
data->motd = nultok(NULL); | |
data->players = atoi(nultok(NULL)); | |
data->maxplayers = atoi(nultok(NULL)); | |
return data; | |
} | |
void decode_free(mc_resp* data) { | |
/* Free the data structure and its underlying memory */ | |
free(data->ptr); | |
free(data); | |
} | |
unsigned long time_diff(struct timeval x , struct timeval y) { | |
time_t x_ms , y_ms , diff; | |
x_ms = x.tv_sec*1000000 + x.tv_usec; | |
y_ms = y.tv_sec*1000000 + y.tv_usec; | |
diff = y_ms - x_ms; | |
return (unsigned long)diff; | |
} | |
int main(int argc, char* argv[]) { | |
int sockfd, n; | |
long i, pings; | |
struct sockaddr_in serv_addr; | |
struct hostent *server; | |
struct timeval before, after; | |
long min, max, avg; | |
/* This buffer will hold the data returned by the server */ | |
char buffer[256]; | |
/* Set default option values. */ | |
struct arguments args; | |
args.port = 25565; | |
args.count = 5; | |
/* Parse arguments into the args struct. */ | |
argp_parse(&argp, argc, argv, 0, 0, &args); | |
/* Get server host */ | |
server = gethostbyname(args.args[0]); | |
if (server == NULL) { | |
fprintf(stderr,"%s: no such host %s\n", argv[0], args.args[0]); | |
exit(0); | |
} | |
/* Set up server address struct */ | |
bzero((char *) &serv_addr, sizeof(serv_addr)); | |
serv_addr.sin_family = AF_INET; | |
bcopy((char *)server->h_addr, | |
(char *)&serv_addr.sin_addr.s_addr, | |
server->h_length); | |
serv_addr.sin_port = htons(args.port); | |
/* Set up stats vars */ | |
avg=0; | |
pings=0; | |
printf("Pinging %s:%d with %ld Minecraft server list ping packets\n", args.args[0], args.port, args.count); | |
for(i=0; i<args.count; i++) { | |
long pingtime; | |
mc_resp* data; | |
/* Sleep 1 second between pings */ | |
if(i) sleep(1); | |
/* Create socket */ | |
sockfd = socket(AF_INET, SOCK_STREAM, 0); | |
if (sockfd < 0) | |
error("Failed to open socket"); | |
/* Set read timeout to 5 seconds */ | |
struct timeval timeout = {5, 0}; | |
if (setsockopt (sockfd, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, sizeof(timeout)) < 0) | |
error("Failed to set socket timeout"); | |
/* Set write timeout to 5 seconds */ | |
if (setsockopt (sockfd, SOL_SOCKET, SO_SNDTIMEO, (char *)&timeout, sizeof(timeout)) < 0) | |
error("Failed to set socket timeout"); | |
/* Now connect */ | |
if (connect(sockfd,(struct sockaddr *) &serv_addr,sizeof(serv_addr)) < 0) { | |
switch(errno) { | |
case ECONNREFUSED: | |
case ETIMEDOUT: | |
case ENETUNREACH: | |
perror(NULL); | |
close(sockfd); | |
continue; | |
case EINPROGRESS: | |
printf("%s\n", strerror(ETIMEDOUT)); | |
close(sockfd); | |
continue; | |
default: | |
/* everything else, exit on error */ | |
error("Failed to connect"); | |
} | |
} | |
/* Clear response buffer */ | |
bzero(buffer,256); | |
/* Send ping request */ | |
n = write(sockfd,"\xFE\x01",2); | |
if (n < 0) | |
error("Failed writing to socket"); | |
/* Start counting and get response*/ | |
gettimeofday(&before , NULL); | |
n = read(sockfd,buffer,255); | |
if (n < 0) | |
error("Failed reading from socket"); | |
/* Stop counting */ | |
gettimeofday(&after , NULL); | |
if(i==0) { | |
/* basic sanity check */ | |
if(n<3 || buffer[0] != '\xff') {fprintf(stderr, "Server returned invalid data, aborting...\n"); exit(1);} | |
/* Check length in endian-agnostic manner */ | |
if((buffer[1]<<8)&buffer[2] != (n-3)) {fprintf(stderr, "Data length verification failed, aborting...\n"); exit(1);} | |
/* Decode response */ | |
data = decode(buffer, n); | |
/* Print response */ | |
printf("Server MOTD: %s\n %d/%d players, running %s (protocol %d)\n", data->motd, data->players, data->maxplayers, data->version, data->proto); | |
decode_free(data); | |
} | |
close(sockfd); | |
pingtime = time_diff(before , after); | |
if(i==0) { | |
min=max=pingtime; | |
} | |
else { | |
if(pingtime < min) min = pingtime; | |
if(pingtime > max) max = pingtime; | |
} | |
printf("Ping response: "); | |
if(pingtime>=100000) printf("%ld ms\n", pingtime/1000, pingtime%1000); | |
else if(pingtime>=10000) printf("%ld.%01ld ms\n", pingtime/1000, (pingtime%1000)/100); | |
else if(pingtime>=1000) printf("%ld.%02ld ms\n", pingtime/1000, (pingtime%1000)/10); | |
else printf("%ld.%03ld ms\n", pingtime/1000, pingtime%1000); | |
avg += pingtime; | |
pings++; | |
} | |
if(pings>0) { | |
avg = avg/pings; | |
printf("rtt min/avg/max %ld.%03ldms/%ld.%03ldms/%ld.%03ldms\n", min/1000, min%1000, avg/1000, avg%1000, max/1000, max%1000); | |
} | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment