|  | /* Copyright 2016 The Roughtime Authors. | 
|  | * | 
|  | * Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | * you may not use this file except in compliance with the License. | 
|  | * You may obtain a copy of the License at | 
|  | * | 
|  | *   http://www.apache.org/licenses/LICENSE-2.0 | 
|  | * | 
|  | * Unless required by applicable law or agreed to in writing, software | 
|  | * distributed under the License is distributed on an "AS IS" BASIS, | 
|  | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | * See the License for the specific language governing permissions and | 
|  | * limitations under the License. */ | 
|  |  | 
|  | // simple_client is the most basic Roughtime client possible. Given a filename | 
|  | // containing a servers list as the sole argument it prints the time obtained | 
|  | // from a single server and the offset from the current system clock. | 
|  |  | 
|  | #include <arpa/inet.h> | 
|  | #include <assert.h> | 
|  | #include <errno.h> | 
|  | #include <inttypes.h> | 
|  | #include <netdb.h> | 
|  | #include <netinet/udp.h> | 
|  | #include <sys/socket.h> | 
|  | #include <sys/types.h> | 
|  | #include <unistd.h> | 
|  |  | 
|  | #include <string> | 
|  |  | 
|  | #include <google/protobuf/stubs/status.h> | 
|  | #include <google/protobuf/util/json_util.h> | 
|  | #include <openssl/rand.h> | 
|  |  | 
|  | #include "client.h" | 
|  | #include "config.pb.h" | 
|  | #include "protocol.h" | 
|  |  | 
|  | // kTimeoutSeconds is the number of seconds that we will wait for a reply | 
|  | // from the server. | 
|  | static const int kTimeoutSeconds = 3; | 
|  |  | 
|  | namespace roughtime { | 
|  |  | 
|  | // MonotonicUs returns the value of the monotonic clock in microseconds. | 
|  | uint64_t MonotonicUs(); | 
|  |  | 
|  | // MonotonicUs returns the value of the realtime clock in microseconds. | 
|  | uint64_t RealtimeUs(); | 
|  |  | 
|  | // GetUsableServer parses the JSON-encoded server information from | 
|  | // |servers_contents| and looks for the first server with an Ed25519 public key | 
|  | // and UDP address. If it finds one, it sets |*out_name|, |*out_address| and | 
|  | // |*out_public_key| and returns true. Otherwise it returns false. | 
|  | static bool GetUsableServer(std::string* out_name, std::string* out_address, | 
|  | std::string* out_public_key, | 
|  | const std::string& servers_contents) { | 
|  | config::ServersJSON servers; | 
|  | google::protobuf::util::Status status = | 
|  | google::protobuf::util::JsonStringToMessage(servers_contents, &servers); | 
|  | if (!status.ok()) { | 
|  | std::string error_message(status.error_message().data(), | 
|  | status.error_message().size()); | 
|  | fprintf(stderr, "Failed to parse servers JSON: %s\n", | 
|  | error_message.c_str()); | 
|  | return false; | 
|  | } | 
|  |  | 
|  | for (int i = 0; i < servers.servers_size(); i++) { | 
|  | const config::Server& server = servers.servers(i); | 
|  |  | 
|  | if (server.public_key_type() != "ed25519") { | 
|  | continue; | 
|  | } | 
|  |  | 
|  | for (int j = 0; j < server.addresses_size(); j++) { | 
|  | const config::ServerAddress& address = server.addresses(j); | 
|  |  | 
|  | if (address.protocol() != "udp") { | 
|  | continue; | 
|  | } | 
|  |  | 
|  | *out_name = server.name(); | 
|  | *out_address = address.address(); | 
|  | *out_public_key = server.public_key(); | 
|  | return true; | 
|  | } | 
|  | } | 
|  |  | 
|  | fprintf(stderr, "Failed to find any usable servers.\n"); | 
|  | return false; | 
|  | } | 
|  |  | 
|  | }  // namespace roughtime | 
|  |  | 
|  | // ReadServersFile reads the contents of |filename| and sets |*out_contents| to | 
|  | // contain them. It returns true on success and false on error. | 
|  | static bool ReadServersFile(std::string* out_contents, const char* filename) { | 
|  | FILE* servers_file = fopen(filename, "r"); | 
|  | if (servers_file == nullptr) { | 
|  | fprintf(stderr, "Failed to open JSON servers file.\n"); | 
|  | return false; | 
|  | } | 
|  |  | 
|  | if (fseek(servers_file, 0, SEEK_END) != 0) { | 
|  | fprintf(stderr, "Failed to seek within JSON servers file.\n"); | 
|  | fclose(servers_file); | 
|  | return false; | 
|  | } | 
|  |  | 
|  | const long length = ftell(servers_file);  // NOLINT | 
|  | if (length < 0) { | 
|  | fprintf(stderr, "Failed to get offset within JSON servers file.\n"); | 
|  | fclose(servers_file); | 
|  | return false; | 
|  | } | 
|  |  | 
|  | if (fseek(servers_file, 0, SEEK_SET) != 0) { | 
|  | fprintf(stderr, "Failed to seek within JSON servers file.\n"); | 
|  | fclose(servers_file); | 
|  | return false; | 
|  | } | 
|  |  | 
|  | std::unique_ptr<uint8_t[]> buf(new uint8_t[length]); | 
|  | if (fread(buf.get(), static_cast<size_t>(length), 1, servers_file) != 1) { | 
|  | fprintf(stderr, "Failed to read JSON servers file.\n"); | 
|  | fclose(servers_file); | 
|  | return false; | 
|  | } | 
|  |  | 
|  | fclose(servers_file); | 
|  | out_contents->assign(reinterpret_cast<const char*>(buf.get()), length); | 
|  | return true; | 
|  | } | 
|  |  | 
|  | // CreateSocket resolves the given address (which must be of the form | 
|  | // "host:port") and sets |*out_socket| to reference a fresh socket connected to | 
|  | // that address. It returns true on success and false on error. | 
|  | static bool CreateSocket(int* out_socket, const std::string& address) { | 
|  | const size_t colon_offset = address.rfind(':'); | 
|  | if (colon_offset == std::string::npos) { | 
|  | fprintf(stderr, "No port number in server address: %s\n", address.c_str()); | 
|  | return false; | 
|  | } | 
|  | std::string host(address.substr(0, colon_offset)); | 
|  | const std::string port_str(address.substr(colon_offset + 1)); | 
|  |  | 
|  | struct addrinfo hints; | 
|  | memset(&hints, 0, sizeof(hints)); | 
|  | hints.ai_socktype = SOCK_DGRAM; | 
|  | hints.ai_protocol = IPPROTO_UDP; | 
|  | hints.ai_flags = AI_NUMERICSERV; | 
|  |  | 
|  | if (!host.empty() && host[0] == '[' && host[host.size() - 1] == ']') { | 
|  | host = host.substr(1, host.size() - 1); | 
|  | hints.ai_family = AF_INET6; | 
|  | hints.ai_flags |= AI_NUMERICHOST; | 
|  | } | 
|  |  | 
|  | struct addrinfo* addrs; | 
|  | int r = getaddrinfo(host.c_str(), port_str.c_str(), &hints, &addrs); | 
|  | if (r != 0) { | 
|  | fprintf(stderr, "Failed to resolve %s: %s", address.c_str(), | 
|  | gai_strerror(r)); | 
|  | return false; | 
|  | } | 
|  |  | 
|  | int sock = socket(addrs->ai_family, addrs->ai_socktype, addrs->ai_protocol); | 
|  | if (sock < 0) { | 
|  | perror("Failed to create UDP socket"); | 
|  | freeaddrinfo(addrs); | 
|  | return false; | 
|  | } | 
|  |  | 
|  | if (connect(sock, addrs->ai_addr, addrs->ai_addrlen)) { | 
|  | perror("Failed to connect UDP socket"); | 
|  | freeaddrinfo(addrs); | 
|  | close(sock); | 
|  | return false; | 
|  | } | 
|  |  | 
|  | char dest_str[INET6_ADDRSTRLEN]; | 
|  | r = getnameinfo(addrs->ai_addr, addrs->ai_addrlen, dest_str, sizeof(dest_str), | 
|  | NULL /* don't want port information */, 0, NI_NUMERICHOST); | 
|  | freeaddrinfo(addrs); | 
|  |  | 
|  | if (r != 0) { | 
|  | fprintf(stderr, "getnameinfo: %s", gai_strerror(r)); | 
|  | close(sock); | 
|  | return false; | 
|  | } | 
|  |  | 
|  | printf("Sending request to %s, port %s.\n", dest_str, port_str.c_str()); | 
|  | *out_socket = sock; | 
|  | return true; | 
|  | } | 
|  |  | 
|  | enum ExitCode { | 
|  | kExitBadSystemTime = 1, | 
|  | kExitBadArguments = 2, | 
|  | kExitNoServer = 3, | 
|  | kExitNetworkError = 4, | 
|  | kExitTimeout = 5, | 
|  | kExitBadReply = 6, | 
|  | }; | 
|  |  | 
|  | int main(int argc, char** argv) { | 
|  | if (argc != 2) { | 
|  | fprintf(stderr, "Usage: %s <roughtime-servers.json>\n", argv[0]); | 
|  | return kExitBadArguments; | 
|  | } | 
|  |  | 
|  | std::string servers_contents; | 
|  | if (!ReadServersFile(&servers_contents, argv[1])) { | 
|  | return kExitBadArguments; | 
|  | } | 
|  |  | 
|  | std::string name, address, public_key; | 
|  | if (!roughtime::GetUsableServer(&name, &address, &public_key, | 
|  | servers_contents)) { | 
|  | return kExitNoServer; | 
|  | } | 
|  |  | 
|  | int fd = 0; | 
|  | if (!CreateSocket(&fd, address)) { | 
|  | return kExitNetworkError; | 
|  | } | 
|  |  | 
|  | uint8_t nonce[roughtime::kNonceLength]; | 
|  | RAND_bytes(nonce, sizeof(nonce)); | 
|  | const std::string request = roughtime::CreateRequest(nonce); | 
|  |  | 
|  | struct timeval timeout; | 
|  | timeout.tv_sec = kTimeoutSeconds; | 
|  | timeout.tv_usec = 0; | 
|  | setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); | 
|  |  | 
|  | ssize_t r; | 
|  | do { | 
|  | r = send(fd, request.data(), request.size(), 0 /* flags */); | 
|  | } while (r == -1 && errno == EINTR); | 
|  | const uint64_t start_us = roughtime::MonotonicUs(); | 
|  |  | 
|  | if (r < 0 || static_cast<size_t>(r) != request.size()) { | 
|  | perror("send on UDP socket"); | 
|  | close(fd); | 
|  | return kExitNetworkError; | 
|  | } | 
|  |  | 
|  | uint8_t recv_buf[roughtime::kMinRequestSize]; | 
|  | ssize_t buf_len; | 
|  | do { | 
|  | buf_len = recv(fd, recv_buf, sizeof(recv_buf), 0 /* flags */); | 
|  | } while (buf_len == -1 && errno == EINTR); | 
|  |  | 
|  | const uint64_t end_us = roughtime::MonotonicUs(); | 
|  | const uint64_t end_realtime_us = roughtime::RealtimeUs(); | 
|  |  | 
|  | close(fd); | 
|  |  | 
|  | if (buf_len == -1) { | 
|  | if (errno == EINTR) { | 
|  | fprintf(stderr, "No response from %s with %d seconds.\n", name.c_str(), | 
|  | kTimeoutSeconds); | 
|  | return kExitTimeout; | 
|  | } | 
|  |  | 
|  | perror("recv from UDP socket"); | 
|  | return kExitNetworkError; | 
|  | } | 
|  |  | 
|  | roughtime::rough_time_t timestamp; | 
|  | uint32_t radius; | 
|  | std::string error; | 
|  | if (!roughtime::ParseResponse( | 
|  | ×tamp, &radius, &error, | 
|  | reinterpret_cast<const uint8_t*>(public_key.data()), recv_buf, | 
|  | buf_len, nonce)) { | 
|  | fprintf(stderr, "Response from %s failed verification: %s", name.c_str(), | 
|  | error.c_str()); | 
|  | return kExitBadReply; | 
|  | } | 
|  |  | 
|  | // We assume that the path to the Roughtime server is symmetric and thus add | 
|  | // half the round-trip time to the server's timestamp to produce our estimate | 
|  | // of the current time. | 
|  | timestamp += (end_us - start_us) / 2; | 
|  |  | 
|  | printf("Received reply in %" PRIu64 "μs.\n", end_us - start_us); | 
|  | printf("Current time is %" PRIu64 "μs from the epoch, ±%uμs \n", timestamp, | 
|  | static_cast<unsigned>(radius)); | 
|  | int64_t system_offset = | 
|  | static_cast<int64_t>(timestamp) - static_cast<int64_t>(end_realtime_us); | 
|  | printf("System clock differs from that estimate by %" PRId64 "μs.\n", | 
|  | system_offset); | 
|  |  | 
|  | static const int64_t kTenMinutes = 10 * 60 * 1000000; | 
|  | if (imaxabs(system_offset) > kTenMinutes) { | 
|  | return kExitBadSystemTime; | 
|  | } | 
|  |  | 
|  | return 0; | 
|  | } |