| /* 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; | |
| } |