/*
* Copyright 2008-2017 by Emeric Vernat
*
* This file is part of Java Melody.
*
* 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 net.bull.javamelody;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.servlet.http.HttpSession;
/**
* Informations sur une session http.
* L'état d'une instance est initialisé à son instanciation et non mutable;
* il est donc de fait thread-safe.
* Cet état est celui d'une session http à un instant t.
* Les instances sont sérialisables pour pouvoir être transmises au serveur de collecte.
* @author Emeric Vernat
*/
class SessionInformations implements Serializable {
static final String SESSION_COUNTRY_KEY = "javamelody.country";
static final String SESSION_REMOTE_ADDR = "javamelody.remoteAddr";
static final String SESSION_REMOTE_USER = "javamelody.remoteUser";
static final String SESSION_USER_AGENT = "javamelody.userAgent";
private static final long serialVersionUID = -2689338895804445093L;
private static final List<String> BROWSERS = Arrays.asList("Edge", "Chrome", "CriOS", "Firefox",
"Safari", "MSIE", "Trident", "Opera" // IEMobile dans MSIE
);
private static final List<String> OS = Arrays.asList(
// Android avant Linux. iPhone, iPad dans Mac OS. *bot et (yahoo) slurp ignorés
"Windows", "Android", "Linux", "Mac OS");
private static final Map<String, String> WINDOWS_CODE_TO_NAME_MAP = new LinkedHashMap<String, String>();
static {
// see https://msdn.microsoft.com/en-us/library/ms537503%28v=vs.85%29.aspx
WINDOWS_CODE_TO_NAME_MAP.put("NT 6.3", "8.1");
WINDOWS_CODE_TO_NAME_MAP.put("NT 6.2", "8");
WINDOWS_CODE_TO_NAME_MAP.put("NT 6.1", "7");
WINDOWS_CODE_TO_NAME_MAP.put("NT 6.0", "Vista");
WINDOWS_CODE_TO_NAME_MAP.put("NT 5.2", "Server 2003/XP");
WINDOWS_CODE_TO_NAME_MAP.put("NT 5.1", "XP");
// others ommitted
}
// on utilise ce ByteArrayOutputStream pour calculer les tailles sérialisées,
// on n'a qu'une instance pour éviter d'instancier un gros tableau d'octets à chaque session
@SuppressWarnings("all")
private static final ByteArrayOutputStream TEMP_OUTPUT = new ByteArrayOutputStream(8 * 1024);
private final String id;
private final Date lastAccess;
private final Date age;
private final Date expirationDate;
private final int attributeCount;
private final boolean serializable;
private final String country;
private final String remoteAddr;
private final String remoteUser;
private final String userAgent;
private final int serializedSize;
@SuppressWarnings("all")
private final List<SessionAttribute> attributes;
static class SessionAttribute implements Serializable {
private static final long serialVersionUID = 4786854834871331127L;
private final String name;
private final String type;
private final String content;
private final boolean serializable;
private final int serializedSize;
SessionAttribute(HttpSession session, String attributeName) {
super();
assert session != null;
assert attributeName != null;
name = attributeName;
final Object value = session.getAttribute(attributeName);
serializable = value == null || value instanceof Serializable;
serializedSize = getObjectSize(value);
if (value == null) {
content = null;
type = null;
} else {
String tmp;
try {
tmp = String.valueOf(value);
} catch (final Exception e) {
tmp = e.toString();
}
content = tmp;
type = value.getClass().getName();
}
}
String getName() {
return name;
}
String getType() {
return type;
}
String getContent() {
return content;
}
boolean isSerializable() {
return serializable;
}
int getSerializedSize() {
return serializedSize;
}
}
SessionInformations(HttpSession session, boolean includeAttributes) {
super();
assert session != null;
id = session.getId();
final long now = System.currentTimeMillis();
lastAccess = new Date(now - session.getLastAccessedTime());
age = new Date(now - session.getCreationTime());
expirationDate = new Date(
session.getLastAccessedTime() + session.getMaxInactiveInterval() * 1000L);
final List<String> attributeNames = Collections.list(session.getAttributeNames());
attributeCount = attributeNames.size();
serializable = computeSerializable(session, attributeNames);
final Object countryCode = session.getAttribute(SESSION_COUNTRY_KEY);
if (countryCode == null) {
country = null;
} else {
country = countryCode.toString().toLowerCase(Locale.ENGLISH);
}
final Object addr = session.getAttribute(SESSION_REMOTE_ADDR);
if (addr == null) {
remoteAddr = null;
} else {
remoteAddr = addr.toString();
}
Object user = session.getAttribute(SESSION_REMOTE_USER);
if (user == null) {
// si getRemoteUser() n'était pas renseigné, on essaye ACEGI_SECURITY_LAST_USERNAME
// (notamment pour Jenkins)
user = session.getAttribute("ACEGI_SECURITY_LAST_USERNAME");
if (user == null) {
// et sinon SPRING_SECURITY_LAST_USERNAME
user = session.getAttribute("SPRING_SECURITY_LAST_USERNAME");
}
}
if (user == null) {
remoteUser = null;
} else {
remoteUser = user.toString();
}
final Object agent = session.getAttribute(SESSION_USER_AGENT);
if (agent == null) {
userAgent = null;
} else {
userAgent = agent.toString();
}
serializedSize = computeSerializedSize(session, attributeNames);
if (includeAttributes) {
attributes = new ArrayList<SessionAttribute>(attributeCount);
for (final String attributeName : attributeNames) {
attributes.add(new SessionAttribute(session, attributeName));
}
} else {
attributes = null;
}
}
private boolean computeSerializable(HttpSession session, List<String> attributeNames) {
for (final String attributeName : attributeNames) {
final Object attributeValue = session.getAttribute(attributeName);
if (!(attributeValue == null || attributeValue instanceof Serializable)) {
return false;
}
}
return true;
}
private int computeSerializedSize(HttpSession session, List<String> attributeNames) {
if (!serializable) {
// la taille pour la session est inconnue si un de ses attributs n'est pas sérialisable
return -1;
}
// On calcule la taille sérialisée de tous les attributs en sérialisant une liste les contenant
// Rq : la taille sérialisée des attributs ensembles peut être très inférieure à la somme
// des tailles sérialisées, car des attributs peuvent référencer des objets communs,
// mais la liste contenant introduit un overhead fixe sur le résultat par rapport à la somme des tailles
// (de même que l'introduirait la sérialisation de l'objet HttpSession avec ses propriétés standards)
// Rq : on ne peut calculer la taille sérialisée exacte de l'objet HttpSession
// car cette interface JavaEE n'est pas déclarée Serializable et l'implémentation au moins dans tomcat
// n'est pas Serializable non plus
// Rq : la taille occupée en mémoire par une session http est un peu différente de la taille sérialisée
// car la sérialisation d'un objet est différente des octets occupés en mémoire
// et car les objets retenus par une session sont éventuellement référencés par ailleurs
// (la fin de la session ne réduirait alors pas l'occupation mémoire autant que la taille de la session)
final List<Serializable> serializableAttributes = new ArrayList<Serializable>(
attributeNames.size());
for (final String attributeName : attributeNames) {
final Object attributeValue = session.getAttribute(attributeName);
serializableAttributes.add((Serializable) attributeValue);
}
return getObjectSize(serializableAttributes);
}
String getId() {
return id;
}
Date getLastAccess() {
return lastAccess;
}
Date getAge() {
return age;
}
Date getExpirationDate() {
return expirationDate;
}
int getAttributeCount() {
return attributeCount;
}
boolean isSerializable() {
return serializable;
}
String getCountry() {
return country;
}
String getCountryDisplay() {
final String myCountry = getCountry();
if (myCountry == null) {
return null;
}
// "fr" est sans conséquence
return new Locale("fr", myCountry).getDisplayCountry(I18N.getCurrentLocale());
}
String getRemoteAddr() {
return remoteAddr;
}
String getBrowser() {
if (userAgent == null) {
return null;
}
final String[] userAgentSplitted = userAgent.split("[ ;]");
for (final String browser : BROWSERS) {
for (final String ua : userAgentSplitted) {
if (ua.contains(browser)) {
final String result = ua.trim();
// for user agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; AS; rv:11.0) like Gecko
// see http://www.useragentstring.com/pages/Internet%20Explorer/
return result.replace("Trident/7.0", "MSIE 11.0");
}
}
}
// browser unknown, the complete userAgent will still be displayed in the session attributes
return null;
}
String getOs() {
if (userAgent == null) {
return null;
}
final String[] userAgentSplitted = userAgent.split("[();]");
for (final String os : OS) {
for (final String ua : userAgentSplitted) {
if (ua.contains(os)) {
String result = ua.trim();
if (result.contains("Windows")) {
for (final Map.Entry<String, String> entry : WINDOWS_CODE_TO_NAME_MAP
.entrySet()) {
final String code = entry.getKey();
final String name = entry.getValue();
result = result.replace(code, name);
}
}
return result;
}
}
}
// OS unknown, the complete userAgent will still be displayed in the session attributes
return null;
}
String getRemoteUser() {
return remoteUser;
}
int getSerializedSize() {
return serializedSize;
}
List<SessionAttribute> getAttributes() {
return Collections.unmodifiableList(attributes);
}
/** {@inheritDoc} */
@Override
public String toString() {
return getClass().getSimpleName() + "[id=" + getId() + ", remoteAddr=" + getRemoteAddr()
+ ", serializedSize=" + getSerializedSize() + ']';
}
static int getObjectSize(Object object) {
if (!(object instanceof Serializable)) {
return -1;
}
final Serializable serializable = (Serializable) object;
// synchronized pour protéger l'accès à TEMP_OUTPUT static
synchronized (TEMP_OUTPUT) {
TEMP_OUTPUT.reset();
try {
final ObjectOutputStream out = new ObjectOutputStream(TEMP_OUTPUT);
try {
out.writeObject(serializable);
} finally {
out.close();
}
return TEMP_OUTPUT.size();
} catch (final Throwable e) { // NOPMD
// ce catch Throwable inclut IOException et aussi NoClassDefFoundError/ClassNotFoundException (issue 355)
return -1;
}
}
}
}