// Copyright (C) 2009 The Android Open Source Project // // 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.gerrit.sshd; import com.google.gerrit.lifecycle.LifecycleListener; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.PeerDaemonUser; import com.google.gerrit.server.config.SitePaths; import com.google.gerrit.server.util.IdGenerator; import com.google.gerrit.sshd.SshScope.Context; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import org.apache.log4j.Appender; import org.apache.log4j.AsyncAppender; import org.apache.log4j.DailyRollingFileAppender; import org.apache.log4j.Layout; import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.apache.log4j.spi.ErrorHandler; import org.apache.log4j.spi.LoggingEvent; import org.eclipse.jgit.util.QuotedString; import java.io.File; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.TimeZone; @Singleton class SshLog implements LifecycleListener { private static final Logger log = Logger.getLogger(SshLog.class); private static final String LOG_NAME = "sshd_log"; private static final String P_SESSION = "session"; private static final String P_USER_NAME = "userName"; private static final String P_ACCOUNT_ID = "accountId"; private static final String P_WAIT = "queueWaitTime"; private static final String P_EXEC = "executionTime"; private static final String P_STATUS = "status"; private final Provider<SshSession> session; private final Provider<Context> context; private final AsyncAppender async; @Inject SshLog(final Provider<SshSession> session, final Provider<Context> context, final SitePaths site) { this.session = session; this.context = context; final DailyRollingFileAppender dst = new DailyRollingFileAppender(); dst.setName(LOG_NAME); dst.setLayout(new MyLayout()); dst.setEncoding("UTF-8"); dst.setFile(new File(resolve(site.logs_dir), LOG_NAME).getPath()); dst.setImmediateFlush(true); dst.setAppend(true); dst.setThreshold(Level.INFO); dst.setErrorHandler(new DieErrorHandler()); dst.activateOptions(); dst.setErrorHandler(new LogLogHandler()); async = new AsyncAppender(); async.setBlocking(true); async.setBufferSize(64); async.setLocationInfo(false); async.addAppender(dst); async.activateOptions(); } @Override public void start() { } @Override public void stop() { async.close(); } void onLogin() { async.append(log("LOGIN FROM " + session.get().getRemoteAddressAsString())); } void onAuthFail(final SshSession sd) { final LoggingEvent event = new LoggingEvent( // Logger.class.getName(), // fqnOfCategoryClass null, // logger (optional) System.currentTimeMillis(), // when Level.INFO, // level "AUTH FAILURE FROM " + sd.getRemoteAddressAsString(), // message text "SSHD", // thread name null, // exception information null, // current NDC string null, // caller location null // MDC properties ); event.setProperty(P_SESSION, id(sd.getSessionId())); event.setProperty(P_USER_NAME, sd.getUsername()); final String error = sd.getAuthenticationError(); if (error != null) { event.setProperty(P_STATUS, error); } async.append(event); } void onExecute(int exitValue) { final Context ctx = context.get(); ctx.finished = System.currentTimeMillis(); final String commandLine = ctx.getCommandLine(); String cmd = QuotedString.BOURNE.quote(commandLine); if (cmd == commandLine) { cmd = "'" + commandLine + "'"; } final LoggingEvent event = log(cmd); event.setProperty(P_WAIT, (ctx.started - ctx.created) + "ms"); event.setProperty(P_EXEC, (ctx.finished - ctx.started) + "ms"); final String status; switch (exitValue) { case BaseCommand.STATUS_CANCEL: status = "killed"; break; case BaseCommand.STATUS_NOT_FOUND: status = "not-found"; break; case BaseCommand.STATUS_NOT_ADMIN: status = "not-admin"; break; default: status = String.valueOf(exitValue); break; } event.setProperty(P_STATUS, status); async.append(event); } void onLogout() { async.append(log("LOGOUT")); } private LoggingEvent log(final String msg) { final SshSession sd = session.get(); final CurrentUser user = sd.getCurrentUser(); final LoggingEvent event = new LoggingEvent( // Logger.class.getName(), // fqnOfCategoryClass null, // logger (optional) System.currentTimeMillis(), // when Level.INFO, // level msg, // message text "SSHD", // thread name null, // exception information null, // current NDC string null, // caller location null // MDC properties ); event.setProperty(P_SESSION, id(sd.getSessionId())); String userName = "-", accountId = "-"; if (user instanceof IdentifiedUser) { IdentifiedUser u = (IdentifiedUser) user; userName = u.getAccount().getUserName(); accountId = "a/" + u.getAccountId().toString(); } else if (user instanceof PeerDaemonUser) { userName = PeerDaemonUser.USER_NAME; } event.setProperty(P_USER_NAME, userName); event.setProperty(P_ACCOUNT_ID, accountId); return event; } private static String id(final int id) { return IdGenerator.format(id); } private static File resolve(final File logs_dir) { try { return logs_dir.getCanonicalFile(); } catch (IOException e) { return logs_dir.getAbsoluteFile(); } } private static final class MyLayout extends Layout { private final Calendar calendar; private long lastTimeMillis; private final char[] lastTimeString = new char[20]; private final char[] timeZone; MyLayout() { final TimeZone tz = TimeZone.getDefault(); calendar = Calendar.getInstance(tz); final SimpleDateFormat sdf = new SimpleDateFormat("Z"); sdf.setTimeZone(tz); timeZone = sdf.format(new Date()).toCharArray(); } @Override public String format(LoggingEvent event) { final StringBuffer buf = new StringBuffer(128); buf.append('['); formatDate(event.getTimeStamp(), buf); buf.append(' '); buf.append(timeZone); buf.append(']'); req(P_SESSION, buf, event); req(P_USER_NAME, buf, event); req(P_ACCOUNT_ID, buf, event); buf.append(' '); buf.append(event.getMessage()); opt(P_WAIT, buf, event); opt(P_EXEC, buf, event); opt(P_STATUS, buf, event); buf.append('\n'); return buf.toString(); } private void formatDate(final long now, final StringBuffer sbuf) { final int millis = (int) (now % 1000); final long rounded = now - millis; if (rounded != lastTimeMillis) { synchronized (calendar) { final int start = sbuf.length(); calendar.setTimeInMillis(rounded); sbuf.append(calendar.get(Calendar.YEAR)); sbuf.append('-'); final int month = calendar.get(Calendar.MONTH) + 1; if (month < 10) sbuf.append('0'); sbuf.append(month); sbuf.append('-'); final int day = calendar.get(Calendar.DAY_OF_MONTH); if (day < 10) sbuf.append('0'); sbuf.append(day); sbuf.append(' '); final int hour = calendar.get(Calendar.HOUR_OF_DAY); if (hour < 10) sbuf.append('0'); sbuf.append(hour); sbuf.append(':'); final int mins = calendar.get(Calendar.MINUTE); if (mins < 10) sbuf.append('0'); sbuf.append(mins); sbuf.append(':'); final int secs = calendar.get(Calendar.SECOND); if (secs < 10) sbuf.append('0'); sbuf.append(secs); sbuf.append(','); sbuf.getChars(start, sbuf.length(), lastTimeString, 0); lastTimeMillis = rounded; } } else { sbuf.append(lastTimeString); } if (millis < 100) { sbuf.append('0'); } if (millis < 10) { sbuf.append('0'); } sbuf.append(millis); } private void req(String key, StringBuffer buf, LoggingEvent event) { Object val = event.getMDC(key); buf.append(' '); if (val != null) { String s = val.toString(); if (0 <= s.indexOf(' ')) { buf.append(QuotedString.BOURNE.quote(s)); } else { buf.append(val); } } else { buf.append('-'); } } private void opt(String key, StringBuffer buf, LoggingEvent event) { Object val = event.getMDC(key); if (val != null) { buf.append(' '); buf.append(val); } } @Override public boolean ignoresThrowable() { return true; } @Override public void activateOptions() { } } private static final class DieErrorHandler implements ErrorHandler { @Override public void error(String message, Exception e, int errorCode, LoggingEvent event) { error(e != null ? e.getMessage() : message); } @Override public void error(String message, Exception e, int errorCode) { error(e != null ? e.getMessage() : message); } @Override public void error(String message) { throw new RuntimeException("Cannot open log file: " + message); } @Override public void activateOptions() { } @Override public void setAppender(Appender appender) { } @Override public void setBackupAppender(Appender appender) { } @Override public void setLogger(Logger logger) { } } private static final class LogLogHandler implements ErrorHandler { @Override public void error(String message, Exception e, int errorCode, LoggingEvent event) { log.error(message, e); } @Override public void error(String message, Exception e, int errorCode) { log.error(message, e); } @Override public void error(String message) { log.error(message); } @Override public void activateOptions() { } @Override public void setAppender(Appender appender) { } @Override public void setBackupAppender(Appender appender) { } @Override public void setLogger(Logger logger) { } } }