| // Copyright 2020 The Bazel Authors. All rights reserved. |
| // |
| // 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. |
| package com.google.devtools.build.lib.authandtls; |
| |
| import static com.google.common.base.Predicates.not; |
| import static java.nio.charset.StandardCharsets.UTF_8; |
| |
| import com.google.auto.value.AutoValue; |
| import com.google.common.base.Strings; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.devtools.build.lib.authandtls.Netrc.Credential; |
| import java.io.BufferedReader; |
| import java.io.Closeable; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.util.ArrayDeque; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.Queue; |
| |
| /** |
| * A parser used to parse .netrc content. |
| * |
| * @see <a href="https://man.cx/netrc(4)">netrc − file for ftp remote login data</a> |
| * @see <a |
| * href="https://github.com/bazelbuild/bazel/blob/master/tools/build_defs/repo/utils.bzl#L203-L204">Starlark |
| * netrc parser</a> |
| */ |
| public class NetrcParser { |
| private static final String MACHINE = "machine"; |
| private static final String MACDEF = "macdef"; |
| private static final String DEFAULT = "default"; |
| private static final String LOGIN = "login"; |
| private static final String PASSWORD = "password"; |
| private static final String ACCOUNT = "account"; |
| |
| interface Token {} |
| |
| @AutoValue |
| abstract static class ItemToken implements Token { |
| public static ItemToken create(String item) { |
| return new AutoValue_NetrcParser_ItemToken(item); |
| } |
| |
| abstract String item(); |
| } |
| |
| @AutoValue |
| abstract static class NewlineToken implements Token { |
| public static NewlineToken create() { |
| return new AutoValue_NetrcParser_NewlineToken(); |
| } |
| } |
| |
| @AutoValue |
| abstract static class CommentToken implements Token { |
| public static CommentToken create() { |
| return new AutoValue_NetrcParser_CommentToken(); |
| } |
| } |
| |
| private static class TokenStream implements Closeable { |
| private final BufferedReader bufferedReader; |
| private final Queue<Token> tokens = new ArrayDeque<>(); |
| |
| TokenStream(InputStream inputStream) throws IOException { |
| bufferedReader = new BufferedReader(new InputStreamReader(inputStream, UTF_8)); |
| processLine(); |
| } |
| |
| @Override |
| public void close() throws IOException { |
| bufferedReader.close(); |
| } |
| |
| private void processLine() throws IOException { |
| String line = bufferedReader.readLine(); |
| if (line == null) { |
| return; |
| } |
| |
| // Comments start with # |
| if (line.startsWith("#")) { |
| tokens.add(CommentToken.create()); |
| } else { |
| Arrays.stream(line.split("\\s+")) |
| .filter(not(Strings::isNullOrEmpty)) |
| .map(ItemToken::create) |
| .forEach(tokens::add); |
| } |
| |
| tokens.add(NewlineToken.create()); |
| } |
| |
| public boolean hasNext() { |
| return !tokens.isEmpty(); |
| } |
| |
| public Token next() throws IOException { |
| Token token = tokens.poll(); |
| if (tokens.isEmpty()) { |
| processLine(); |
| } |
| return token; |
| } |
| |
| public Token peek() { |
| return tokens.peek(); |
| } |
| } |
| |
| private NetrcParser() {} |
| |
| public static Netrc parseAndClose(InputStream inputStream) throws IOException { |
| try (TokenStream tokenStream = new TokenStream(inputStream)) { |
| return parse(tokenStream); |
| } |
| } |
| |
| private static Netrc parse(TokenStream tokenStream) throws IOException { |
| Credential defaultCredential = null; |
| Map<String, Credential> credentialMap = new HashMap<>(); |
| |
| boolean done = false; |
| while (!done && tokenStream.hasNext()) { |
| Token token = tokenStream.next(); |
| if (token instanceof ItemToken itemToken) { |
| String item = itemToken.item(); |
| switch (item) { |
| case MACHINE -> { |
| String machine = nextItem(tokenStream); |
| Credential credential = parseCredentialForMachine(tokenStream, machine); |
| credentialMap.put(machine, credential); |
| } |
| case MACDEF -> skipMacdef(tokenStream); |
| case DEFAULT -> { |
| defaultCredential = parseCredentialForMachine(tokenStream, DEFAULT); |
| // There can be only one default token, and it must be after all machine tokens. |
| done = true; |
| } |
| default -> |
| throw new IOException( |
| String.format( |
| "Unexpected token: %s (expecting %s, %s or %s)", |
| item, MACHINE, MACDEF, DEFAULT)); |
| } |
| } |
| } |
| |
| return Netrc.create(defaultCredential, ImmutableMap.copyOf(credentialMap)); |
| } |
| |
| private static String nextItem(TokenStream tokenStream) throws IOException { |
| while (tokenStream.hasNext()) { |
| Token token = tokenStream.next(); |
| if (token instanceof ItemToken itemToken) { |
| return itemToken.item(); |
| } |
| } |
| |
| throw new IOException("Unexpected EOF"); |
| } |
| |
| /** Parse credentials for a given machine from token stream. */ |
| private static Credential parseCredentialForMachine(TokenStream tokenStream, String machine) |
| throws IOException { |
| Credential.Builder builder = Credential.builder(machine); |
| |
| boolean done = false; |
| while (!done && tokenStream.hasNext()) { |
| // Peek rather than taking next token since we probably won't process it |
| Token token = tokenStream.peek(); |
| if (token instanceof ItemToken itemToken) { |
| String item = itemToken.item(); |
| switch (item) { |
| case LOGIN -> { |
| tokenStream.next(); |
| builder.setLogin(nextItem(tokenStream)); |
| } |
| case PASSWORD -> { |
| tokenStream.next(); |
| builder.setPassword(nextItem(tokenStream)); |
| } |
| case ACCOUNT -> { |
| tokenStream.next(); |
| builder.setAccount(nextItem(tokenStream)); |
| } |
| case MACHINE, MACDEF, DEFAULT -> done = true; |
| default -> |
| throw new IOException( |
| String.format( |
| "Unexpected item: %s (expecting %s, %s, %s, %s, %s or %s)", |
| item, LOGIN, PASSWORD, ACCOUNT, MACHINE, MACDEF, DEFAULT)); |
| } |
| } else { |
| tokenStream.next(); |
| } |
| } |
| |
| return builder.build(); |
| } |
| |
| /** Skip macdef section since we don't need that data currently. */ |
| private static void skipMacdef(TokenStream tokenStream) throws IOException { |
| int numNewlines = 0; |
| while (tokenStream.hasNext()) { |
| Token token = tokenStream.next(); |
| if (token instanceof NewlineToken) { |
| ++numNewlines; |
| } else { |
| numNewlines = 0; |
| } |
| if (numNewlines >= 2) { |
| break; |
| } |
| } |
| } |
| } |