diff options
| author | zhuzhenjun <[email protected]> | 2023-09-16 10:43:06 +0800 |
|---|---|---|
| committer | zhuzhenjun <[email protected]> | 2023-09-21 15:05:11 +0800 |
| commit | 91e6b79afc817f06b570b48fca67d92690bb7d27 (patch) | |
| tree | 0db3fff1fb3f843df792350bd7fe6abd42a17ab6 /example | |
| parent | e9b190b0697703f5e8f8ba7550ff1918deccbc72 (diff) | |
v0.0.0v0.0.0
Diffstat (limited to 'example')
| -rw-r--r-- | example/Makefile.am | 12 | ||||
| -rw-r--r-- | example/osfp_example.c | 655 | ||||
| -rw-r--r-- | example/osfp_match.c | 129 |
3 files changed, 662 insertions, 134 deletions
diff --git a/example/Makefile.am b/example/Makefile.am index c700652..f6fc004 100644 --- a/example/Makefile.am +++ b/example/Makefile.am @@ -1,11 +1,13 @@ -bin_PROGRAMS = osfp_match +bin_PROGRAMS = osfp_example -osfp_match_SOURCES = \ - osfp_match.c +osfp_example_SOURCES = \ + osfp_example.c -osfp_match_LDADD = \ +osfp_example_LDADD = \ ../src/.libs/libosfp.la -osfp_match_LDFLAGS = \ +osfp_example_LDFLAGS = \ -lpcap +osfp_example_CFLAGS = \ + -I../src diff --git a/example/osfp_example.c b/example/osfp_example.c new file mode 100644 index 0000000..a6c9ab8 --- /dev/null +++ b/example/osfp_example.c @@ -0,0 +1,655 @@ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <time.h> + +#include <linux/in.h> +#include <linux/if_ether.h> +#include <linux/ip.h> +#include <linux/ipv6.h> +#include <linux/tcp.h> + +#include <sys/socket.h> + +#include <pcap.h> + +#include "libosfp.h" +#include "libosfp_fingerprint.h" + +#define DEFAULT_FP_FILE "./fp.json" + +#define ETHERNET_HEADER_LEN 14 +#define VLAN_HEADER_LEN 4 +#define IPV4_HEADER_LEN 20 +#define TCP_HEADER_LEN 20 +#define IPV6_HEADER_LEN 40 + +#define VLAN_MAX_LAYER 2 + + +/* Port is just a uint16_t */ +typedef uint16_t Port; +#define SET_PORT(v, p) ((p) = (v)) +#define COPY_PORT(a,b) ((b) = (a)) + +/* Address */ +typedef struct Address_ { + char family; + union { + uint32_t address_un_data32[4]; /* type-specific field */ + uint16_t address_un_data16[8]; /* type-specific field */ + uint8_t address_un_data8[16]; /* type-specific field */ + struct in6_addr address_un_in6; + } address; +} Address; + +#define addr_data32 address.address_un_data32 +#define addr_data16 address.address_un_data16 +#define addr_data8 address.address_un_data8 +#define addr_in6addr address.address_un_in6 + +#define COPY_ADDRESS(a, b) do { \ + (b)->family = (a)->family; \ + (b)->addr_data32[0] = (a)->addr_data32[0]; \ + (b)->addr_data32[1] = (a)->addr_data32[1]; \ + (b)->addr_data32[2] = (a)->addr_data32[2]; \ + (b)->addr_data32[3] = (a)->addr_data32[3]; \ + } while (0) + +/* Set the IPv4 addresses into the Addrs of the Packet. + * Make sure p->ip4h is initialized and validated. + * + * We set the rest of the struct to 0 so we can + * prevent using memset. */ +#define SET_IPV4_SRC_ADDR(p, a) do { \ + (a)->family = AF_INET; \ + (a)->addr_data32[0] = (uint32_t)(p)->iph->saddr; \ + (a)->addr_data32[1] = 0; \ + (a)->addr_data32[2] = 0; \ + (a)->addr_data32[3] = 0; \ + } while (0) + +#define SET_IPV4_DST_ADDR(p, a) do { \ + (a)->family = AF_INET; \ + (a)->addr_data32[0] = (uint32_t)(p)->iph->daddr; \ + (a)->addr_data32[1] = 0; \ + (a)->addr_data32[2] = 0; \ + (a)->addr_data32[3] = 0; \ + } while (0) + +/* Set the IPv6 addresses into the Addrs of the Packet. + * Make sure p->ip6h is initialized and validated. */ +#define SET_IPV6_SRC_ADDR(p, a) do { \ + (a)->family = AF_INET6; \ + (a)->addr_data32[0] = (p)->ip6h->saddr.s6_addr32[0]; \ + (a)->addr_data32[1] = (p)->ip6h->saddr.s6_addr32[1]; \ + (a)->addr_data32[2] = (p)->ip6h->saddr.s6_addr32[2]; \ + (a)->addr_data32[3] = (p)->ip6h->saddr.s6_addr32[3]; \ + } while (0) + +#define SET_IPV6_DST_ADDR(p, a) do { \ + (a)->family = AF_INET6; \ + (a)->addr_data32[0] = (p)->ip6h->daddr.s6_addr32[0]; \ + (a)->addr_data32[1] = (p)->ip6h->daddr.s6_addr32[1]; \ + (a)->addr_data32[2] = (p)->ip6h->daddr.s6_addr32[2]; \ + (a)->addr_data32[3] = (p)->ip6h->daddr.s6_addr32[3]; \ + } while (0) + +#define TCP_GET_RAW_SRC_PORT(tcph) ntohs((tcph)->source) +#define TCP_GET_RAW_DST_PORT(tcph) ntohs((tcph)->dest) + +#define TCP_GET_SRC_PORT(p) TCP_GET_RAW_SRC_PORT((p)->tcph) +#define TCP_GET_DST_PORT(p) TCP_GET_RAW_DST_PORT((p)->tcph) + +/* Set the TCP ports into the Ports of the Packet. + * Make sure p->tcph is initialized and validated. */ +#define SET_TCP_SRC_PORT(pkt, prt) do { \ + SET_PORT(TCP_GET_SRC_PORT((pkt)), *(prt)); \ + } while (0) + +#define SET_TCP_DST_PORT(pkt, prt) do { \ + SET_PORT(TCP_GET_DST_PORT((pkt)), *(prt)); \ + } while (0) + +#define GET_IPV4_SRC_ADDR_U32(p) ((p)->src.addr_data32[0]) +#define GET_IPV4_DST_ADDR_U32(p) ((p)->dst.addr_data32[0]) +#define GET_IPV4_SRC_ADDR_PTR(p) ((p)->src.addr_data32) +#define GET_IPV4_DST_ADDR_PTR(p) ((p)->dst.addr_data32) + +#define GET_IPV6_SRC_IN6ADDR(p) ((p)->src.addr_in6addr) +#define GET_IPV6_DST_IN6ADDR(p) ((p)->dst.addr_in6addr) +#define GET_IPV6_SRC_ADDR(p) ((p)->src.addr_data32) +#define GET_IPV6_DST_ADDR(p) ((p)->dst.addr_data32) +#define GET_TCP_SRC_PORT(p) ((p)->sp) +#define GET_TCP_DST_PORT(p) ((p)->dp) + + +typedef struct Packet_ { + struct ethhdr *ethh; + struct iphdr *iph; + struct ipv6hdr *ip6h; + struct tcphdr *tcph; + + Address src; + Address dst; + union { + Port sp; + // icmp type and code of this packet + struct { + uint8_t type; + uint8_t code; + } icmp_s; + }; + union { + Port dp; + // icmp type and code of the expected counterpart (for flows) + struct { + uint8_t type; + uint8_t code; + } icmp_d; + }; + + int vlan_layer; +} Packet; + + +unsigned char *fp_file; +unsigned char *if_name; +unsigned char *pcap_file_name; +unsigned char *bpf_string; +pcap_t *pcap_handle; + +int processed_packet; +int link_type; + +void usage(void) { + fprintf(stderr, + "Usage: osfp_match [ ...options... ] [ 'filter rule' ]\n" + "\n" + "Network interface options:\n" + "\n" + " -i iface - listen on the specified network interface\n" + " -r file - read offline pcap data from a given file\n" + " -f file - read fingerprint database from 'file' (%s)\n", + DEFAULT_FP_FILE); + exit(1); +} + +typedef struct EthernetHdr_ { + uint8_t eth_dst[6]; + uint8_t eth_src[6]; + uint16_t eth_type; +} __attribute__((__packed__)) EthernetHdr; + + +int packet_decode_tcp(Packet *p, const unsigned char *data, unsigned int len) +{ + int ret = -1; + int tcp_hdr_len; + struct tcphdr *tcph; + + if (len < TCP_HEADER_LEN) { + goto exit; + } + + tcph = (struct tcphdr *)data; + tcp_hdr_len = tcph->doff << 2; + + if (len < tcp_hdr_len) { + goto exit; + } + + p->tcph = tcph; + SET_TCP_SRC_PORT(p,&p->sp); + SET_TCP_DST_PORT(p,&p->dp); + + ret = 0; +exit: + return ret; +} + +int packet_decode_ipv4(Packet *p, const unsigned char *data, unsigned int len) +{ + int ret = -1; + int ip_total_len, ip_hdr_len; + struct iphdr *iph; + + if (len < IPV4_HEADER_LEN) { + goto exit; + } + + iph = (struct iphdr *)data; + ip_total_len = ntohs(iph->tot_len); + ip_hdr_len = iph->ihl << 2; + + if (ip_hdr_len < IPV4_HEADER_LEN) { + goto exit; + } + + if (ip_total_len < ip_hdr_len) { + goto exit; + } + + if (len < ip_total_len) { + goto exit; + } + + p->iph = iph; + /* set the address struct */ + SET_IPV4_SRC_ADDR(p,&p->src); + SET_IPV4_DST_ADDR(p,&p->dst); + + switch (p->iph->protocol) { + case IPPROTO_TCP: + packet_decode_tcp(p, data + ip_hdr_len, ip_total_len - ip_hdr_len); + break; + default: + goto exit; + } + + ret = 0; +exit: + return ret; +} + +int packet_decode_ipv6(Packet *p, const unsigned char *data, unsigned int len) +{ + int ret = -1; + unsigned short ip6_payload_len; + unsigned char ip6_nexthdr; + struct ipv6hdr *ip6h; + + if (len < IPV6_HEADER_LEN) { + goto exit; + } + + ip6h = (struct ipv6hdr *)data; + ip6_payload_len = ntohs(ip6h->payload_len); + ip6_nexthdr = ip6h->nexthdr; + + if (len < IPV6_HEADER_LEN + ip6_payload_len) { + goto exit; + } + + p->ip6h = ip6h; + SET_IPV6_SRC_ADDR(p,&p->src); + SET_IPV6_DST_ADDR(p,&p->dst); + + switch (ip6_nexthdr) { + case IPPROTO_TCP: + packet_decode_tcp(p, data + IPV6_HEADER_LEN, ip6_payload_len); + break; + default: + goto exit; + } + + ret = 0; +exit: + return ret; +} + +int pakcet_decode_network_layer(Packet *p, const unsigned char *data, unsigned int len, unsigned short proto) +{ + switch (proto) { + case ETH_P_IP: { + packet_decode_ipv4(p, data, len); + break; + } + case ETH_P_IPV6: { + packet_decode_ipv6(p, data, len); + break; + } + case ETH_P_8021Q: + if (p->vlan_layer > VLAN_MAX_LAYER || len < VLAN_HEADER_LEN) { + return -1; + } + unsigned short vlan_proto = ntohs(*(unsigned short*)(data + 2)); + pakcet_decode_network_layer(p, data + VLAN_HEADER_LEN, len - VLAN_HEADER_LEN, vlan_proto); + break; + default: + printf("L3 proto type: %02x not yet supported\n", proto); + break; + } + + return 0; +} + +int packet_decode_ethernet(Packet *p, const unsigned char *data, unsigned int len) +{ + int ret = -1; + unsigned short proto; + struct ethhdr *ethh; + + if (len < ETHERNET_HEADER_LEN) { + goto exit; + } + + ethh = (struct ethhdr *)data; + proto = ntohs(ethh->h_proto); + + p->ethh = ethh; + + ret = pakcet_decode_network_layer(p, data + ETHERNET_HEADER_LEN, len - ETHERNET_HEADER_LEN, proto); + if (ret != 0) { + goto exit; + } + + return 0; +exit: + return ret; +} + +void packet_decode_link_layer(Packet *p, const unsigned char *data, unsigned int len, int datalink) +{ + switch (datalink) { + case DLT_EN10MB: + packet_decode_ethernet(p, data, len); + break; + default: + printf("Datalink type: %02x not yet supported\n", link_type); + pcap_breakloop(pcap_handle); + break; + } +} + +void packet_decode(Packet *p, const unsigned char *data, unsigned int len, int datalink) +{ + packet_decode_link_layer(p, data, len, datalink); +} + +size_t strlcat(char *dst, const char *src, size_t siz) +{ + register char *d = dst; + register const char *s = src; + register size_t n = siz; + size_t dlen; + + /* Find the end of dst and adjust bytes left but don't go past end */ + while (n-- != 0 && *d != '\0') + d++; + dlen = d - dst; + n = siz - dlen; + + if (n == 0) + return(dlen + strlen(s)); + while (*s != '\0') { + if (n != 1) { + *d++ = *s; + n--; + } + s++; + } + *d = '\0'; + + return(dlen + (s - src)); /* count does not include NUL */ +} + +static const char *PrintInetIPv6(const void *src, char *dst, socklen_t size) +{ + int i; + char s_part[6]; + uint16_t x[8]; + memcpy(&x, src, 16); + + /* current IPv6 format is fixed size */ + if (size < 8 * 5) { + printf("Too small buffer to write IPv6 address"); + return NULL; + } + memset(dst, 0, size); + for(i = 0; i < 8; i++) { + snprintf(s_part, sizeof(s_part), "%04x:", htons(x[i])); + strlcat(dst, s_part, size); + } + /* suppress last ':' */ + dst[strlen(dst) - 1] = 0; + + return dst; +} + +const char *PrintInet(int af, const void *src, char *dst, socklen_t size) +{ + switch (af) { + case AF_INET: + snprintf(dst, size, "%u.%u.%u.%u", + ((unsigned char *)src)[0], + ((unsigned char *)src)[1], + ((unsigned char *)src)[2], + ((unsigned char *)src)[3]); + return dst; + case AF_INET6: + /* Format IPv6 without deleting zeroes */ + return PrintInetIPv6(src, dst, size); + default: + printf("Unsupported protocol: %d", af); + } + return NULL; +} + +void example_header_match(libosfp_context_t *libosfp_context, Packet *p) +{ + // tcp/ip header match + int ret; + char str_buf[1024]; + + unsigned char *iph = (unsigned char *)(p->iph != NULL ? (void *)p->iph : (void *)p->ip6h); + unsigned char *tcph = (unsigned char *)p->tcph; + libosfp_result_t result; + + printf("Example header match: --------------------------\n"); + + ret = libosfp_header_match(libosfp_context, iph, tcph, &result); + if (ret != 0) { + printf("libosfp header match failed, erro: %s\n", "?"); + goto exit; + } + + char srcip[46] = {0}, dstip[46] = {0}; + Port sp, dp; + if (p->iph) { + PrintInet(AF_INET, (const void *)&(p->src.addr_data32[0]), srcip, sizeof(srcip)); + PrintInet(AF_INET, (const void *)&(p->dst.addr_data32[0]), dstip, sizeof(dstip)); + } else if (p->ip6h) { + PrintInet(AF_INET6, (const void *)&(p->src.address), srcip, sizeof(srcip)); + PrintInet(AF_INET6, (const void *)&(p->dst.address), dstip, sizeof(dstip)); + } + sp = p->sp; + dp = p->dp; + + printf("Connection info: %s:%d -> %s:%d\n", srcip, sp, dstip, dp); + printf("Most likely os class: %s\n", libosfp_result_likely_os_class_name_get(&result)); + printf("Likely score: %u/100\n", libosfp_result_likely_os_class_score_get(&result)); + + libosfp_result_to_buf(&result, str_buf, sizeof(str_buf)); + fprintf(stdout, "%s\n", str_buf); + +exit: + return; +} + +void example_fingerprint_match(libosfp_context_t *libosfp_context, Packet *p) +{ + // fingerprint match + int ret; + char str_buf[1024]; + + unsigned char *iph = (unsigned char *)(p->iph != NULL ? (void *)p->iph : (void *)p->ip6h); + unsigned char *tcph = (unsigned char *)p->tcph; + libosfp_result_t result; + libosfp_fingerprint_t fp; + + printf("Example fingerprint match: --------------------------\n"); + + ret = libosfp_fingerprinting(iph, tcph, &fp); + if (ret != 0) { + printf("libosfp fingerprinting failed\n"); + goto exit; + } + + libosfp_fingerprint_to_json_buf(&fp, str_buf, sizeof(str_buf)); + fprintf(stdout, "%s\n", str_buf); + + ret = libosfp_score_db_score(libosfp_context, &fp, &result); + if (ret != 0) { + printf("libosfp fingerprint score failed, error: %d\n", ret); + goto exit; + } + + printf("Connection info: %s\n", ""); + printf("Most likely os class: %s\n", libosfp_result_likely_os_class_name_get(&result)); + printf("Likely score: %u/100\n", libosfp_result_likely_os_class_score_get(&result)); + + libosfp_result_to_buf(&result, str_buf, sizeof(str_buf)); + fprintf(stdout, "%s\n", str_buf); + +exit: + return; +} + +void process_packet(char *user, struct pcap_pkthdr *h, u_char *pkt) +{ + int ret; + libosfp_context_t *libosfp_context = (libosfp_context_t *)user; + Packet packet = {0}, *p = &packet; + + // decode packet + packet_decode(p, pkt, h->len, link_type); + if (p->tcph == NULL || (p->iph == NULL && p->ip6h == NULL)) { + goto exit; + } + + // only for tcp syn request packet + if (!p->tcph->syn || p->tcph->ack) { + goto exit; + } + + example_header_match(libosfp_context, p); + + example_fingerprint_match(libosfp_context, p); + + printf("--------------------------- processed packet count %d\n", ++processed_packet); + +exit: + return; +} + +int main(int argc, char *argv[]) +{ + int r; + + while ((r = getopt(argc, argv, "+f:i:r:")) != -1) { + switch(r) { + case 'f': + if (fp_file) { + printf("Multiple -f options not supported.\n"); + exit(1); + } + fp_file = (unsigned char*)optarg; + break; + case 'i': + if (if_name) { + printf("Multiple -i options not supported.\n"); + exit(1); + } + if_name = (unsigned char*)optarg; + break; + case 'r': + if (pcap_file_name) { + printf("Multiple -r options not supported.\n"); + exit(1); + } + pcap_file_name = (unsigned char*)optarg; + break; + default: + usage(); + break; + } + } + + if (optind < argc) { + if (optind + 1 == argc) { + bpf_string = argv[optind]; + } else { + printf("Filter rule must be a single parameter (use quotes).\n"); + exit(1); + } + } + + // prepare pcap handle + + char pcap_err[PCAP_ERRBUF_SIZE]; + + if (pcap_file_name) { + if (access((char*)pcap_file_name, R_OK)) { + printf("No such file: %s\n", pcap_file_name); + exit(1); + } + pcap_handle = pcap_open_offline((char*)pcap_file_name, pcap_err); + if (pcap_handle == NULL ) { + printf("Pcap file open failed. File name: %s, Err: %s\n", pcap_file_name, pcap_err); + exit(1); + } + } else if (if_name) { + pcap_handle = pcap_open_live((char*)if_name, 65535, 1, 5, pcap_err); + if (pcap_handle == NULL) { + printf("Pcap live open failed. Interface name: %s, Err: %s\n", if_name, pcap_err); + exit(1); + } + } else { + usage(); + } + + // setup bpf filter + if (bpf_string) { + struct bpf_program bpf_filter; + + if (pcap_compile(pcap_handle, &bpf_filter, bpf_string, 1, 0) < 0) { + printf("bpf compilation error %s", pcap_geterr(pcap_handle)); + exit(1); + } + + if (pcap_setfilter(pcap_handle, &bpf_filter) < 0) { + printf("could not set bpf filter %s", pcap_geterr(pcap_handle)); + pcap_freecode(&bpf_filter); + exit(1); + } + pcap_freecode(&bpf_filter); + } + + // get link type + link_type = pcap_datalink(pcap_handle); + + // create libosfp context + if (fp_file == NULL) { + fp_file = DEFAULT_FP_FILE; + } + + libosfp_context_t *libosfp_context = libosfp_context_create(fp_file); + if (libosfp_context == NULL) { + printf("could not create libosfp context. fingerprints file: %s\n", fp_file); + exit(1); + } + + // setup libosfp context + r = libosfp_context_setup(libosfp_context); + if (r != LIBOSFP_NOERR) { + printf("could not setup libosfp context. error: %d\n", LIBOSFP_NOERR); + exit(1); + } + + // loop + while (1) { + int r = pcap_dispatch(pcap_handle, 0, (pcap_handler)process_packet, (void*)libosfp_context); + if (r < 0) { + printf("error code: %d, error: %s\n", r, pcap_geterr(pcap_handle)); + break; + } + } + + // create libosfp context + libosfp_context_destroy(libosfp_context); + + return 0; +} + diff --git a/example/osfp_match.c b/example/osfp_match.c deleted file mode 100644 index efde9c2..0000000 --- a/example/osfp_match.c +++ /dev/null @@ -1,129 +0,0 @@ -#include <stdio.h> -#include <time.h> -#include <pcap.h> -#include <netinet/in.h> -#include <netinet/if_ether.h> -#include <unistd.h> -#include <stdlib.h> - -unsigned char *fp_file; -unsigned char *if_name; -unsigned char *pcap_file_name; -unsigned char *bpf_string; - -int processed_packet; - -void usage(void) { - fprintf(stderr, - "Usage: osfp_match [ ...options... ] [ 'filter rule' ]\n" - "\n" - "Network interface options:\n" - "\n" - " -i iface - listen on the specified network interface\n" - " -r file - read offline pcap data from a given file\n" - " -f file - read fingerprint database from 'file' (%s)\n" - ); - exit(1); -} - -void process_packet(char *user, struct pcap_pkthdr *h, u_char *pkt) -{ - printf("packet count %d\n", ++processed_packet); -} - -int main(int argc, char *argv[]) -{ - int r; - - while ((r = getopt(argc, argv, "+f:i:r")) != -1) { - switch(r) { - case 'f': - if (fp_file) { - printf("Multiple -f options not supported.\n"); - exit(1); - } - fp_file = (unsigned char*)optarg; - break; - case 'i': - if (if_name) { - printf("Multiple -i options not supported.\n"); - exit(1); - } - if_name = (unsigned char*)optarg; - break; - case 'r': - if (pcap_file_name) { - printf("Multiple -r options not supported.\n"); - exit(1); - } - pcap_file_name = (unsigned char*)optarg; - break; - default: - usage(); - break; - } - } - - if (optind < argc) { - if (optind + 1 == argc) { - bpf_string = argv[optind]; - } else { - printf("Filter rule must be a single parameter (use quotes).\n"); - exit(1); - } - } - - // prepare pcap handle - - char pcap_err[PCAP_ERRBUF_SIZE]; - pcap_t *pcap_handle; - - if (pcap_file_name) { - if (access((char*)pcap_file_name, R_OK)) { - printf("No such file: %s\n", pcap_file_name); - exit(1); - } - pcap_handle = pcap_open_offline((char*)pcap_file_name, pcap_err); - if (pcap_handle == NULL ) { - printf("Pcap file open failed. File name: %s, Err: %s\n", pcap_file_name, pcap_err); - exit(1); - } - } else if (if_name) { - pcap_handle = pcap_open_live((char*)if_name, 65535, 1, 5, pcap_err); - if (pcap_handle == NULL) { - printf("Pcap live open failed. Interface name: %s, Err: %s\n", if_name, pcap_err); - exit(1); - } - } else { - usage(); - } - - // setup bpf filter - if (bpf_string) { - struct bpf_program bpf_filter; - - if (pcap_compile(pcap_handle, &bpf_filter, bpf_string, 1, 0) < 0) { - printf("bpf compilation error %s", pcap_geterr(pcap_handle)); - exit(1); - } - - if (pcap_setfilter(pcap_handle, &bpf_filter) < 0) { - printf("could not set bpf filter %s", pcap_geterr(pcap_handle)); - pcap_freecode(&bpf_filter); - exit(1); - } - pcap_freecode(&bpf_filter); - } - - // loop - while (1) { - int r = pcap_dispatch(pcap_handle, 0, (pcap_handler)process_packet, NULL); - if (r < 0) { - printf("error code: %d, error: %s\n", r, pcap_geterr(pcap_handle)); - break; - } - } - - return 0; -} - |
