tor-commits
Threads by month
- ----- 2026 -----
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2025 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2024 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2023 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2022 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2021 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2020 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2019 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2018 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2017 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2016 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2015 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2014 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2013 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2012 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2011 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- 1 participants
- 216104 discussions
[metrics-db/master] Merge Torperf files into new .tpf file format.
by karsten@torproject.org 29 May '12
by karsten@torproject.org 29 May '12
29 May '12
commit 8b45d2fb00af489e4ff9ebb74a892e5f91485844
Author: Karsten Loesing <karsten.loesing(a)gmx.net>
Date: Thu May 24 09:34:26 2012 +0200
Merge Torperf files into new .tpf file format.
Implements part of #3036.
---
config.template | 12 +-
src/org/torproject/ernie/db/Configuration.java | 39 +--
src/org/torproject/ernie/db/Main.java | 3 +-
src/org/torproject/ernie/db/TorperfDownloader.java | 484 ++++++++++++++++++--
4 files changed, 467 insertions(+), 71 deletions(-)
diff --git a/config.template b/config.template
index 5034e12..aecc67d 100644
--- a/config.template
+++ b/config.template
@@ -136,11 +136,9 @@
## times)
#TorperfSource torperf http://torperf.torproject.org/
#
-## Torperf .data files available on a given source (option can be
-## contained multiple times)
-#TorperfDataFiles torperf 50kb.data 1mb.data 5mb.data
-#
-## Torperf .extradata files available on a given source (option can be
-## contained multiple times)
-#TorperfExtradataFiles torperf 50kb.extradata 1mb.extradata 5mb.extradata
+## Torperf measurement file size in bytes, .data file, and .extradata file
+## available on a given source (option can be contained multiple times)
+#TorperfFiles torperf 51200 50kb.data 50kb.extradata
+#TorperfFiles torperf 1048576 1mb.data 1mb.extradata
+#TorperfFiles torperf 5242880 5mb.data 5mb.extradata
diff --git a/src/org/torproject/ernie/db/Configuration.java b/src/org/torproject/ernie/db/Configuration.java
index 6ab9f44..63349c8 100644
--- a/src/org/torproject/ernie/db/Configuration.java
+++ b/src/org/torproject/ernie/db/Configuration.java
@@ -59,8 +59,7 @@ public class Configuration {
private boolean processTorperfFiles = false;
private String torperfOutputDirectory = "torperf/";
private SortedMap<String, String> torperfSources = null;
- private SortedMap<String, List<String>> torperfDataFiles = null;
- private SortedMap<String, List<String>> torperfExtradataFiles = null;
+ private List<String> torperfFiles = null;
private boolean provideFilesViaRsync = false;
private String rsyncDirectory = "rsync";
public Configuration() {
@@ -194,29 +193,18 @@ public class Configuration {
String sourceName = parts[1];
String baseUrl = parts[2];
this.torperfSources.put(sourceName, baseUrl);
- } else if (line.startsWith("TorperfDataFiles")) {
- if (this.torperfDataFiles == null) {
- this.torperfDataFiles = new TreeMap<String, List<String>>();
+ } else if (line.startsWith("TorperfFiles")) {
+ if (this.torperfFiles == null) {
+ this.torperfFiles = new ArrayList<String>();
}
String[] parts = line.split(" ");
- String sourceName = parts[1];
- List<String> dataFiles = new ArrayList<String>();
- for (int i = 2; i < parts.length; i++) {
- dataFiles.add(parts[i]);
- }
- this.torperfDataFiles.put(sourceName, dataFiles);
- } else if (line.startsWith("TorperfExtradataFiles")) {
- if (this.torperfExtradataFiles == null) {
- this.torperfExtradataFiles =
- new TreeMap<String, List<String>>();
- }
- String[] parts = line.split(" ");
- String sourceName = parts[1];
- List<String> extradataFiles = new ArrayList<String>();
- for (int i = 2; i < parts.length; i++) {
- extradataFiles.add(parts[i]);
+ if (parts.length != 5) {
+ logger.severe("Configuration file contains TorperfFiles "
+ + "option with wrong number of values in line '" + line
+ + "'! Exiting!");
+ System.exit(1);
}
- this.torperfExtradataFiles.put(sourceName, extradataFiles);
+ this.torperfFiles.add(line);
} else if (line.startsWith("ProvideFilesViaRsync")) {
this.provideFilesViaRsync = Integer.parseInt(
line.split(" ")[1]) != 0;
@@ -381,11 +369,8 @@ public class Configuration {
public SortedMap<String, String> getTorperfSources() {
return this.torperfSources;
}
- public SortedMap<String, List<String>> getTorperfDataFiles() {
- return this.torperfDataFiles;
- }
- public SortedMap<String, List<String>> getTorperfExtradataFiles() {
- return this.torperfExtradataFiles;
+ public List<String> getTorperfFiles() {
+ return this.torperfFiles;
}
public boolean getProvideFilesViaRsync() {
return this.provideFilesViaRsync;
diff --git a/src/org/torproject/ernie/db/Main.java b/src/org/torproject/ernie/db/Main.java
index e44eac0..4fa9e37 100644
--- a/src/org/torproject/ernie/db/Main.java
+++ b/src/org/torproject/ernie/db/Main.java
@@ -139,8 +139,7 @@ public class Main {
// Process Torperf files
if (config.getProcessTorperfFiles()) {
new TorperfDownloader(new File(config.getTorperfOutputDirectory()),
- config.getTorperfSources(), config.getTorperfDataFiles(),
- config.getTorperfExtradataFiles());
+ config.getTorperfSources(), config.getTorperfFiles());
}
// Copy recently published files to a local directory that can then
diff --git a/src/org/torproject/ernie/db/TorperfDownloader.java b/src/org/torproject/ernie/db/TorperfDownloader.java
index 8ee0fe9..e7c55a1 100644
--- a/src/org/torproject/ernie/db/TorperfDownloader.java
+++ b/src/org/torproject/ernie/db/TorperfDownloader.java
@@ -11,65 +11,172 @@ import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
+import java.text.SimpleDateFormat;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
+import java.util.TimeZone;
+import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
/* Download possibly truncated Torperf .data and .extradata files from
- * configured sources and append them to the files we already have. */
+ * configured sources, append them to the files we already have, and merge
+ * the two files into the .tpf format. */
public class TorperfDownloader {
private File torperfOutputDirectory = null;
private SortedMap<String, String> torperfSources = null;
- private SortedMap<String, List<String>> torperfDataFiles = null;
- private SortedMap<String, List<String>> torperfExtradataFiles = null;
+ private List<String> torperfFilesLines = null;
private Logger logger = null;
+ private SimpleDateFormat dateFormat;
public TorperfDownloader(File torperfOutputDirectory,
SortedMap<String, String> torperfSources,
- SortedMap<String, List<String>> torperfDataFiles,
- SortedMap<String, List<String>> torperfExtradataFiles) {
+ List<String> torperfFilesLines) {
if (torperfOutputDirectory == null) {
throw new IllegalArgumentException();
}
this.torperfOutputDirectory = torperfOutputDirectory;
this.torperfSources = torperfSources;
- this.torperfDataFiles = torperfDataFiles;
- this.torperfExtradataFiles = torperfExtradataFiles;
+ this.torperfFilesLines = torperfFilesLines;
if (!this.torperfOutputDirectory.exists()) {
this.torperfOutputDirectory.mkdirs();
}
this.logger = Logger.getLogger(TorperfDownloader.class.getName());
- this.downloadAndMergeFiles(this.torperfDataFiles, true);
- this.downloadAndMergeFiles(this.torperfExtradataFiles, false);
+ this.dateFormat = new SimpleDateFormat("yyyy-MM-dd");
+ this.dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ this.readLastMergedTimestamps();
+ for (String torperfFilesLine : this.torperfFilesLines) {
+ this.downloadAndMergeFiles(torperfFilesLine);
+ }
+ this.writeLastMergedTimestamps();
}
- private void downloadAndMergeFiles(
- SortedMap<String, List<String>> dataOrExtradataFiles,
- boolean isDataFile) {
- for (Map.Entry<String, List<String>> e :
- dataOrExtradataFiles.entrySet()) {
- String sourceName = e.getKey();
- String sourceBaseUrl = torperfSources.get(sourceName);
- List<String> files = e.getValue();
- for (String file : files) {
- String url = sourceBaseUrl + file;
- File outputFile = new File(torperfOutputDirectory,
- sourceName + "-" + file);
- this.downloadAndMergeFile(url, outputFile, isDataFile);
+ private File torperfLastMergedFile =
+ new File("stats/torperf-last-merged");
+ SortedMap<String, String> lastMergedTimestamps =
+ new TreeMap<String, String>();
+ private void readLastMergedTimestamps() {
+ if (!this.torperfLastMergedFile.exists()) {
+ return;
+ }
+ try {
+ BufferedReader br = new BufferedReader(new FileReader(
+ this.torperfLastMergedFile));
+ String line;
+ while ((line = br.readLine()) != null) {
+ String[] parts = line.split(" ");
+ String fileName = null, timestamp = null;
+ if (parts.length == 2) {
+ try {
+ Double.parseDouble(parts[1]);
+ fileName = parts[0];
+ timestamp = parts[1];
+ } catch (NumberFormatException e) {
+ /* Handle below. */
+ }
+ }
+ if (fileName == null || timestamp == null) {
+ this.logger.log(Level.WARNING, "Invalid line '" + line + "' in "
+ + this.torperfLastMergedFile.getAbsolutePath() + ". "
+ + "Ignoring past history of merging .data and .extradata "
+ + "files.");
+ this.lastMergedTimestamps.clear();
+ break;
+ }
+ this.lastMergedTimestamps.put(fileName, timestamp);
}
+ br.close();
+ } catch (IOException e) {
+ this.logger.log(Level.WARNING, "Error while reading '"
+ + this.torperfLastMergedFile.getAbsolutePath() + ". Ignoring "
+ + "past history of merging .data and .extradata files.");
+ this.lastMergedTimestamps.clear();
+ }
+ }
+
+ private void writeLastMergedTimestamps() {
+ try {
+ BufferedWriter bw = new BufferedWriter(new FileWriter(
+ this.torperfLastMergedFile));
+ for (Map.Entry<String, String> e :
+ this.lastMergedTimestamps.entrySet()) {
+ String fileName = e.getKey();
+ String timestamp = e.getValue();
+ bw.write(fileName + " " + timestamp + "\n");
+ }
+ bw.close();
+ } catch (IOException e) {
+ this.logger.log(Level.WARNING, "Error while writing '"
+ + this.torperfLastMergedFile.getAbsolutePath() + ". This may "
+ + "result in ignoring history of merging .data and .extradata "
+ + "files in the next execution.", e);
+ }
+ }
+
+ private void downloadAndMergeFiles(String torperfFilesLine) {
+ String[] parts = torperfFilesLine.split(" ");
+ String sourceName = parts[1];
+ int fileSize = -1;
+ try {
+ fileSize = Integer.parseInt(parts[2]);
+ } catch (NumberFormatException e) {
+ this.logger.log(Level.WARNING, "Could not parse file size in "
+ + "TorperfFiles configuration line '" + torperfFilesLine
+ + "'.");
+ return;
+ }
+
+ /* Download and append the .data file. */
+ String dataFileName = parts[3];
+ String sourceBaseUrl = torperfSources.get(sourceName);
+ String dataUrl = sourceBaseUrl + dataFileName;
+ String dataOutputFileName = sourceName + "-" + dataFileName;
+ File dataOutputFile = new File(torperfOutputDirectory,
+ dataOutputFileName);
+ boolean downloadedDataFile = this.downloadAndAppendFile(dataUrl,
+ dataOutputFile, true);
+
+ /* Download and append the .extradata file. */
+ String extradataFileName = parts[4];
+ String extradataUrl = sourceBaseUrl + extradataFileName;
+ String extradataOutputFileName = sourceName + "-" + extradataFileName;
+ File extradataOutputFile = new File(torperfOutputDirectory,
+ extradataOutputFileName);
+ boolean downloadedExtradataFile = this.downloadAndAppendFile(
+ extradataUrl, extradataOutputFile, false);
+
+ /* Merge both files into .tpf format. */
+ if (!downloadedDataFile && !downloadedExtradataFile) {
+ return;
+ }
+ String skipUntil = null;
+ if (this.lastMergedTimestamps.containsKey(dataOutputFileName)) {
+ skipUntil = this.lastMergedTimestamps.get(dataOutputFileName);
+ }
+ try {
+ skipUntil = this.mergeFiles(dataOutputFile, extradataOutputFile,
+ sourceName, fileSize, skipUntil);
+ } catch (IOException e) {
+ this.logger.log(Level.WARNING, "Failed merging " + dataOutputFile
+ + " and " + extradataOutputFile + ".", e);
+ }
+ if (skipUntil != null) {
+ this.lastMergedTimestamps.put(dataOutputFileName, skipUntil);
}
}
- private void downloadAndMergeFile(String url, File outputFile,
+ private boolean downloadAndAppendFile(String url, File outputFile,
boolean isDataFile) {
+
+ /* Read an existing output file to determine which line will be the
+ * first to append to it. */
String lastTimestampLine = null;
int linesAfterLastTimestampLine = 0;
if (outputFile.exists() && outputFile.lastModified() >
System.currentTimeMillis() - 330L * 60L * 1000L) {
- return;
+ return false;
} else if (outputFile.exists()) {
try {
BufferedReader br = new BufferedReader(new FileReader(
@@ -85,10 +192,10 @@ public class TorperfDownloader {
}
br.close();
} catch (IOException e) {
- logger.log(Level.WARNING, "Failed reading '"
- + outputFile.getAbsolutePath() + "' to find the last line to "
- + "append to.", e);
- return;
+ this.logger.log(Level.WARNING, "Failed reading '"
+ + outputFile.getAbsolutePath() + "' to determine the first "
+ + "line to append to it.", e);
+ return false;
}
}
try {
@@ -120,14 +227,14 @@ public class TorperfDownloader {
bw.close();
br.close();
} catch (IOException e) {
- logger.log(Level.WARNING, "Failed downloading and merging '" + url
- + "'.", e);
- return;
+ this.logger.log(Level.WARNING, "Failed downloading and/or merging '"
+ + url + "'.", e);
+ return false;
}
if (lastTimestampLine == null) {
- logger.warning("'" + outputFile.getAbsolutePath() + "' doesn't "
- + "contain any timestamp lines. Unable to check whether that "
- + "file is stale or not.");
+ this.logger.warning("'" + outputFile.getAbsolutePath()
+ + "' doesn't contain any timestamp lines. Unable to check "
+ + "whether that file is stale or not.");
} else {
long lastTimestampMillis = -1L;
if (isDataFile) {
@@ -141,11 +248,318 @@ public class TorperfDownloader {
}
if (lastTimestampMillis < System.currentTimeMillis()
- 330L * 60L * 1000L) {
- logger.warning("The last timestamp in '"
+ this.logger.warning("The last timestamp in '"
+ outputFile.getAbsolutePath() + "' is more than 5:30 hours "
+ "old: " + lastTimestampMillis);
}
}
+ return true;
+ }
+
+ private String mergeFiles(File dataFile, File extradataFile,
+ String source, int fileSize, String skipUntil) throws IOException {
+ SortedMap<String, String> config = new TreeMap<String, String>();
+ config.put("SOURCE", source);
+ config.put("FILESIZE", String.valueOf(fileSize));
+ if (!dataFile.exists() || !extradataFile.exists()) {
+ this.logger.warning("File " + dataFile.getAbsolutePath() + " or "
+ + extradataFile.getAbsolutePath() + " is missing.");
+ return null;
+ }
+ this.logger.fine("Merging " + dataFile.getAbsolutePath() + " and "
+ + extradataFile.getAbsolutePath() + " into .tpf format.");
+ BufferedReader brD = new BufferedReader(new FileReader(dataFile)),
+ brE = new BufferedReader(new FileReader(extradataFile));
+ String lineD = brD.readLine(), lineE = brE.readLine();
+ int d = 1, e = 1;
+ String maxDataComplete = null, maxUsedAt = null;
+ while (lineD != null) {
+
+ /* Parse .data line. Every valid .data line will go into the .tpf
+ * format, either with additional information from the .extradata
+ * file or without it. */
+ if (lineD.isEmpty()) {
+ this.logger.finer("Skipping empty line " + dataFile.getName() + ":"
+ + d++ + ".");
+ lineD = brD.readLine();
+ continue;
+ }
+ SortedMap<String, String> data = this.parseDataLine(lineD);
+ if (data == null) {
+ this.logger.warning("Skipping illegal line .data:" + d++ + " '"
+ + lineD + "'.");
+ lineD = brD.readLine();
+ continue;
+ }
+ String dataComplete = data.get("DATACOMPLETE");
+ double dataCompleteSeconds = Double.parseDouble(dataComplete);
+ if (skipUntil != null && dataComplete.compareTo(skipUntil) < 0) {
+ this.logger.finer("Skipping " + dataFile.getName() + ":"
+ + d++ + " which we already processed before.");
+ lineD = brD.readLine();
+ continue;
+ }
+ maxDataComplete = dataComplete;
+
+ /* Parse .extradata line if available and try to find the one that
+ * matches the .data line. */
+ SortedMap<String, String> extradata = null;
+ while (lineE != null) {
+ if (lineE.isEmpty()) {
+ this.logger.finer("Skipping " + extradataFile.getName() + ":"
+ + e++ + " which is empty.");
+ lineE = brE.readLine();
+ continue;
+ }
+ if (lineE.startsWith("BUILDTIMEOUT_SET ")) {
+ this.logger.finer("Skipping " + extradataFile.getName() + ":"
+ + e++ + " which is a BUILDTIMEOUT_SET line.");
+ lineE = brE.readLine();
+ continue;
+ } else if (lineE.startsWith("ok ") ||
+ lineE.startsWith("error ")) {
+ this.logger.finer("Skipping " + extradataFile.getName() + ":"
+ + e++ + " which is in the old format.");
+ lineE = brE.readLine();
+ continue;
+ }
+ extradata = this.parseExtradataLine(lineE);
+ if (extradata == null) {
+ this.logger.warning("Skipping Illegal line .extradata:" + e++
+ + " '" + lineE + "'.");
+ lineE = brE.readLine();
+ continue;
+ }
+ if (!extradata.containsKey("USED_AT")) {
+ this.logger.finer("Skipping " + extradataFile.getName() + ":"
+ + e++ + " which doesn't contain a USED_AT element.");
+ lineE = brE.readLine();
+ continue;
+ }
+ String usedAt = extradata.get("USED_AT");
+ double usedAtSeconds = Double.parseDouble(usedAt);
+ if (skipUntil != null && usedAt.compareTo(skipUntil) < 0) {
+ this.logger.finer("Skipping " + extradataFile.getName() + ":"
+ + e++ + " which we already processed before.");
+ lineE = brE.readLine();
+ continue;
+ }
+ maxUsedAt = usedAt;
+ if (Math.abs(usedAtSeconds - dataCompleteSeconds) <= 1.0) {
+ this.logger.fine("Merging " + extradataFile.getName() + ":"
+ + e++ + " into the current .data line.");
+ lineE = brE.readLine();
+ break;
+ } else if (usedAtSeconds > dataCompleteSeconds) {
+ this.logger.finer("Comparing " + extradataFile.getName()
+ + " to the next .data line.");
+ extradata = null;
+ break;
+ } else {
+ this.logger.finer("Skipping " + extradataFile.getName() + ":"
+ + e++ + " which is too old to be merged with .data:" + d
+ + ".");
+ lineE = brE.readLine();
+ continue;
+ }
+ }
+
+ /* Write output line to .tpf file. */
+ SortedMap<String, String> keysAndValues =
+ new TreeMap<String, String>();
+ if (extradata != null) {
+ keysAndValues.putAll(extradata);
+ }
+ keysAndValues.putAll(data);
+ keysAndValues.putAll(config);
+ this.logger.fine("Writing " + dataFile.getName() + ":" + d++ + ".");
+ lineD = brD.readLine();
+ try {
+ this.writeTpfLine(source, fileSize, keysAndValues);
+ } catch (IOException ex) {
+ this.logger.log(Level.WARNING, "Error writing output line. "
+ + "Aborting to merge " + dataFile.getName() + " and "
+ + extradataFile.getName() + ".", e);
+ break;
+ }
+ }
+ brD.close();
+ brE.close();
+ this.writeCachedTpfLines();
+ if (maxDataComplete == null) {
+ return maxUsedAt;
+ } else if (maxUsedAt == null) {
+ return maxDataComplete;
+ } else if (maxDataComplete.compareTo(maxUsedAt) > 0) {
+ return maxUsedAt;
+ } else {
+ return maxDataComplete;
+ }
+ }
+
+ private SortedMap<Integer, String> dataTimestamps;
+ private SortedMap<String, String> parseDataLine(String line) {
+ String[] parts = line.trim().split(" ");
+ if (line.length() == 0 || parts.length < 20) {
+ return null;
+ }
+ if (this.dataTimestamps == null) {
+ this.dataTimestamps = new TreeMap<Integer, String>();
+ this.dataTimestamps.put(0, "START");
+ this.dataTimestamps.put(2, "SOCKET");
+ this.dataTimestamps.put(4, "CONNECT");
+ this.dataTimestamps.put(6, "NEGOTIATE");
+ this.dataTimestamps.put(8, "REQUEST");
+ this.dataTimestamps.put(10, "RESPONSE");
+ this.dataTimestamps.put(12, "DATAREQUEST");
+ this.dataTimestamps.put(14, "DATARESPONSE");
+ this.dataTimestamps.put(16, "DATACOMPLETE");
+ this.dataTimestamps.put(21, "DATAPERC10");
+ this.dataTimestamps.put(23, "DATAPERC20");
+ this.dataTimestamps.put(25, "DATAPERC30");
+ this.dataTimestamps.put(27, "DATAPERC40");
+ this.dataTimestamps.put(29, "DATAPERC50");
+ this.dataTimestamps.put(31, "DATAPERC60");
+ this.dataTimestamps.put(33, "DATAPERC70");
+ this.dataTimestamps.put(35, "DATAPERC80");
+ this.dataTimestamps.put(37, "DATAPERC90");
+ }
+ SortedMap<String, String> data = new TreeMap<String, String>();
+ try {
+ for (Map.Entry<Integer, String> e : this.dataTimestamps.entrySet()) {
+ int i = e.getKey();
+ if (parts.length > i + 1) {
+ String key = e.getValue();
+ String value = String.format("%s.%02d", parts[i],
+ Integer.parseInt(parts[i + 1]) / 10000);
+ data.put(key, value);
+ }
+ }
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ data.put("WRITEBYTES", parts[18]);
+ data.put("READBYTES", parts[19]);
+ if (parts.length >= 21) {
+ data.put("DIDTIMEOUT", parts[20]);
+ }
+ return data;
+ }
+
+ private SortedMap<String, String> parseExtradataLine(String line) {
+ String[] parts = line.split(" ");
+ SortedMap<String, String> extradata = new TreeMap<String, String>();
+ String previousKey = null;
+ for (String part : parts) {
+ String[] keyAndValue = part.split("=", -1);
+ if (keyAndValue.length == 2) {
+ String key = keyAndValue[0];
+ previousKey = key;
+ String value = keyAndValue[1];
+ if (value.contains(".") && value.lastIndexOf(".") ==
+ value.length() - 2) {
+ /* Make sure that all floats have two trailing digits. */
+ value += "0";
+ }
+ extradata.put(key, value);
+ } else if (keyAndValue.length == 1 && previousKey != null) {
+ String value = keyAndValue[0];
+ if (previousKey.equals("STREAM_FAIL_REASONS") &&
+ (value.equals("MISC") || value.equals("EXITPOLICY") ||
+ value.equals("RESOURCELIMIT") ||
+ value.equals("RESOLVEFAILED"))) {
+ extradata.put(previousKey, extradata.get(previousKey) + ":"
+ + value);
+ } else {
+ return null;
+ }
+ } else {
+ return null;
+ }
+ }
+ return extradata;
+ }
+
+ private String cachedSource;
+ private int cachedFileSize;
+ private String cachedStartDate;
+ private SortedMap<String, String> cachedTpfLines;
+ private void writeTpfLine(String source, int fileSize,
+ SortedMap<String, String> keysAndValues) throws IOException {
+ StringBuilder sb = new StringBuilder();
+ int written = 0;
+ for (Map.Entry<String, String> keyAndValue :
+ keysAndValues.entrySet()) {
+ String key = keyAndValue.getKey();
+ String value = keyAndValue.getValue();
+ sb.append((written++ > 0 ? " " : "") + key + "=" + value);
+ }
+ String line = sb.toString();
+ String startString = keysAndValues.get("START");
+ long startMillis = Long.parseLong(startString.substring(0,
+ startString.indexOf("."))) * 1000L;
+ String startDate = dateFormat.format(startMillis);
+ if (this.cachedTpfLines == null || !source.equals(this.cachedSource) ||
+ fileSize != this.cachedFileSize ||
+ !startDate.equals(this.cachedStartDate)) {
+ this.writeCachedTpfLines();
+ this.readTpfLinesToCache(source, fileSize, startDate);
+ }
+ if (!this.cachedTpfLines.containsKey(startString) ||
+ line.length() > this.cachedTpfLines.get(startString).length()) {
+ this.cachedTpfLines.put(startString, line);
+ }
+ }
+
+ private void readTpfLinesToCache(String source, int fileSize,
+ String startDate) throws IOException {
+ this.cachedTpfLines = new TreeMap<String, String>();
+ this.cachedSource = source;
+ this.cachedFileSize = fileSize;
+ this.cachedStartDate = startDate;
+ File tpfFile = new File(torperfOutputDirectory,
+ startDate.replaceAll("-", "/") + "/"
+ + source + "-" + String.valueOf(fileSize) + "-" + startDate
+ + ".tpf");
+ if (!tpfFile.exists()) {
+ return;
+ }
+ BufferedReader br = new BufferedReader(new FileReader(tpfFile));
+ String line;
+ while ((line = br.readLine()) != null) {
+ if (line.startsWith("@type ")) {
+ continue;
+ }
+ if (line.contains("START=")) {
+ String startString = line.substring(line.indexOf("START=")
+ + "START=".length()).split(" ")[0];
+ this.cachedTpfLines.put(startString, line);
+ }
+ }
+ br.close();
+ }
+
+ private void writeCachedTpfLines() throws IOException {
+ if (this.cachedSource == null || this.cachedFileSize == 0 ||
+ this.cachedStartDate == null || this.cachedTpfLines == null) {
+ return;
+ }
+ File tpfFile = new File(torperfOutputDirectory,
+ this.cachedStartDate.replaceAll("-", "/")
+ + "/" + this.cachedSource + "-"
+ + String.valueOf(this.cachedFileSize) + "-"
+ + this.cachedStartDate + ".tpf");
+ tpfFile.getParentFile().mkdirs();
+ BufferedWriter bw = new BufferedWriter(new FileWriter(tpfFile));
+ bw.write("@type torperf 1.0\n");
+ for (String line : this.cachedTpfLines.values()) {
+ bw.write(line + "\n");
+ }
+ bw.close();
+ this.cachedSource = null;
+ this.cachedFileSize = 0;
+ this.cachedStartDate = null;
+ this.cachedTpfLines = null;
}
}
1
0
[metrics-db/master] Print out a warning if we're missing Torperf data lines.
by karsten@torproject.org 29 May '12
by karsten@torproject.org 29 May '12
29 May '12
commit a77d67de24ab52655479c94a243633f4d4245cdb
Author: Karsten Loesing <karsten.loesing(a)gmx.net>
Date: Tue May 29 20:14:51 2012 +0200
Print out a warning if we're missing Torperf data lines.
---
src/org/torproject/ernie/db/TorperfDownloader.java | 7 +++++++
1 files changed, 7 insertions(+), 0 deletions(-)
diff --git a/src/org/torproject/ernie/db/TorperfDownloader.java b/src/org/torproject/ernie/db/TorperfDownloader.java
index e7c55a1..ddd202a 100644
--- a/src/org/torproject/ernie/db/TorperfDownloader.java
+++ b/src/org/torproject/ernie/db/TorperfDownloader.java
@@ -226,6 +226,13 @@ public class TorperfDownloader {
}
bw.close();
br.close();
+ if (!copyLines) {
+ this.logger.warning("The last timestamp line in '"
+ + outputFile.getAbsolutePath() + "' is not contained in the "
+ + "new file downloaded from '" + url + "'. Cannot append "
+ + "new lines without possibly leaving a gap. Skipping.");
+ return false;
+ }
} catch (IOException e) {
this.logger.log(Level.WARNING, "Failed downloading and/or merging '"
+ url + "'.", e);
1
0
commit f56f83083a27df106b1aaef02cbcafab31a726de
Author: Translation commit bot <translation(a)torproject.org>
Date: Tue May 29 11:45:06 2012 +0000
Update translations for tsum
---
ru/short-user-manual_ru_noimg.xhtml | 4 ++--
1 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/ru/short-user-manual_ru_noimg.xhtml b/ru/short-user-manual_ru_noimg.xhtml
index 491f21d..1b3abf1 100644
--- a/ru/short-user-manual_ru_noimg.xhtml
+++ b/ru/short-user-manual_ru_noimg.xhtml
@@ -23,8 +23,8 @@
<h3 id="tor-for-smartphones">Tor для смартфонов</h3>
<p>Вы можете настроить Tor на своём Android-устройстве, установив пакет, который называется <em>Orbot</em>. Справка по его загрузке и установке есть на <a href="https://www.torproject.org/docs/android.html.en">Сайте Проекта Tor</a>.</p>
<p>We also have experimental packages for <a href="https://www.torproject.org/docs/N900.html.en">Nokia Maemo/N900</a> and <a href="http://sid77.slackware.it/ios/">Apple iOS</a>.</p>
- <h3 id="how-to-verify-that-you-have-the-right-version">How to verify that you have the right version</h3>
- <p>Before running the Tor Browser Bundle, you should make sure that you have the right version.</p>
+ <h3 id="how-to-verify-that-you-have-the-right-version">Как проверить, та ли у Вас версия</h3>
+ <p>Прежде, чем запускать Пакет Tor-Браузера, следует удостовериться в том, что у Вас находится правильная его версия.</p>
<p>The software you receive is accompanied by a file with the same name as the bundle and the extension <strong>.asc</strong>. This .asc file is a GPG signature, and will allow you to verify the file you've downloaded is exactly the one that we intended you to get.</p>
<p>Before you can verify the signature, you will have to download and install GnuPG:</p>
<p><strong>Windows</strong>: <a href="http://gpg4win.org/download.html">http://gpg4win.org/download.html</a><br /><strong>Mac OS X</strong>: <a href="http://www.gpgtools.org/">http://www.gpgtools.org/</a><br /><strong>Linux</strong>: Most Linux distributions come with GnuPG preinstalled.</p>
1
0
commit 0aad0315cdd571c6a827af97f0c8f39f16f41cd1
Author: Translation commit bot <translation(a)torproject.org>
Date: Tue May 29 11:15:07 2012 +0000
Update translations for tsum
---
ru/short-user-manual_ru_noimg.xhtml | 22 +++++++++++-----------
1 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/ru/short-user-manual_ru_noimg.xhtml b/ru/short-user-manual_ru_noimg.xhtml
index 9a8817f..491f21d 100644
--- a/ru/short-user-manual_ru_noimg.xhtml
+++ b/ru/short-user-manual_ru_noimg.xhtml
@@ -5,23 +5,23 @@
</head>
<body>
<h1 id="the-short-user-manual">Краткое Руководство Пользователя</h1>
- <p>Это пользовательское руководство расскажет о том, как загрузить Tor, как им пользоваться, и что предпринимать в случаях, когда Tor не может соединиться с сетью. Если у Вас не получилось найти в этом документе ответ на свой вопрос, не стесняйтесь написать нам по адресу help(a)rt.torproject.org.</p>
+ <p>Это пользовательское руководство расскажет о том, как скачать Tor, как им пользоваться, и что предпринимать в случаях, когда Tor не может соединиться с сетью. Если у Вас не получилось найти в этом документе ответ на свой вопрос, не стесняйтесь написать нам по адресу help(a)rt.torproject.org.</p>
<p>Пожалуйста, помните, что мы осуществляем техподдержку на добровольных началах, причём каждый Божий день нам приходит куча писем с вопросами. Поэтому не волнуйтесь, если мы медлим с ответом на Ваше письмо.</p>
<h2 id="how-tor-works">Как работает Tor</h2>
<p>Tor - это сеть виртуальных туннелей, которая позволяет Вам лучше защищать неприкосновенность Вашей частной жизни и свою безопасность в Интернете. Работает это вот как: Tor соединяет Ваш компьютер с Интернетом не напрямую, а через цепочку из трёх выбранных наугад компьютеров (т.н. <em>ретрансляторы</em>) которые принадлежат к сети Tor.</p>
<p>На рисунке выше изображено, что происходит, когда пользователь посещает различные веб-сайты через Tor. Зелёные мониторы - это ретрансляторы сети Tor, а три ключика - это последовательные слои шифрования между пользователем и каждым следующим ретранслятором.</p>
<p>Всё, что вы посылаете в Интернет через Tor, становится, во-первых анонимным (скрывается источник), а во-вторых, остаётся зашифрованным на всём пути между Вашим компьютером и последним ретранслятором. Но после того, как данные покидают последний ретранслятор и отправляются к интернет-адресу своего назначения - они идут уже не в зашифрованном, а обычном, открытом виде.</p>
<p>Если вы передаёте особо важные данные - например, вводите логин и пароль для входа на веб-сайт - убедитесь, что работает протокол HTTPS (т.е. в адресной строке написано, например, <strong>https</strong>://torproject.org/, а не <strong>http</strong>://torproject.org/).</p>
- <h2 id="how-to-download-tor">Как загрузить Tor</h2>
- <p>Пакет программ, который мы обычно рекомендуем - это <a href="https://www.torproject.org/projects/torbrowser.html">Tor Browser Bundle</a>. Этот пакет не требует установки и содержит браузер, заранее настроенный таким образом, чтобы безопасно выходить в интернет через Tor. Вы просто загружаете пакет, распаковываете архив и запускаете Tor.</p>
- <p>There are two different ways to get hold of the Tor software. You can either browse to the <a href="https://www.torproject.org/">Tor Project website</a> and download it there, or you can use GetTor, the email autoresponder.</p>
- <h3 id="how-to-get-tor-via-email">How to get Tor via email</h3>
- <p>To receive the English Tor Browser Bundle for Windows, send an email to gettor(a)torproject.org with <strong>windows</strong> in the body of the message. You can leave the subject blank.</p>
- <p>You can also request the Tor Browser Bundle for Mac OS X (write <strong>macos-i386</strong>), and Linux (write <strong>linux-i386</strong> for 32-bit systems or <strong>linux-x86_64</strong> for 64-bit systems).</p>
- <p>If you want a translated version of Tor, write <strong>help</strong> instead. You will then receive an email with instructions and a list of available languages.</p>
- <p><strong>Note</strong>: The Tor Browser Bundles for Linux and Mac OS X are rather large, and you will not be able to receive any of these bundles with a Gmail, Hotmail or Yahoo account. If you cannot receive the bundle you want, send an email to help(a)rt.torproject.org and we will give you a list of website mirrors to use.</p>
- <h3 id="tor-for-smartphones">Tor for smartphones</h3>
- <p>You can get Tor on your Android device by installing the package named <em>Orbot</em>. For information about how to download and install Orbot, please see the <a href="https://www.torproject.org/docs/android.html.en">Tor Project website</a>.</p>
+ <h2 id="how-to-download-tor">Как скачать Tor</h2>
+ <p>Пакет программ, который мы обычно рекомендуем - это <a href="https://www.torproject.org/projects/torbrowser.html">Пакет Tor-Браузера</a>. Этот пакет не требует установки и содержит браузер, заранее настроенный таким образом, чтобы безопасно выходить в интернет через Tor. Вы просто загружаете пакет, распаковываете архив и запускаете Tor.</p>
+ <p>Есть два разных способа заполучить программное обеспечение Tor. Вы можете либо открыть в браузере <a href="https://www.torproject.org/">Сайт Проекта Tor</a> и скачать программу оттуда, либо вы можете воспользоваться GetTor - нашим автоответчиком электронной почты.</p>
+ <h3 id="how-to-get-tor-via-email">Как получить Tor по электронной почте</h3>
+ <p>Чтобы получить Английский Пакет Tor-Браузера для Windows, отправьте письмо на адрес электронной почты gettor(a)torproject.org со словом <strong>windows</strong> в теле сообщения. Поле "Тема" можете оставить пустым.</p>
+ <p>Подобным же образом Вы можете запросить Пакет Tor-Браузера для Mac OS (написав <strong>macos-i386</strong>), и для Linux (написав <strong>linux-i386</strong> для 32-битных систем или <strong>linux-x86_64</strong> для 64-битных систем)</p>
+ <p>Если Вам нужна переведённая версия Tor, напишите в письме <strong>help</strong>. Вам придёт ответное письмо с инструкциями и списком доступных языков.</p>
+ <p><strong>Заметка</strong>: Пакет Tor-Браузера для Linux и Mac OS X имеют значительный размер, так что Вы не сможете получить эти пакеты по электронной почте через Gmail, Hotmail или Yahoo. Если у Вас не выходит заполучить необходимый Вам пакет, напишите письмо на адрес help(a)rt.torproject.org и мы дадим Вам список сайтов-зеркал для скачивания этого пакета.</p>
+ <h3 id="tor-for-smartphones">Tor для смартфонов</h3>
+ <p>Вы можете настроить Tor на своём Android-устройстве, установив пакет, который называется <em>Orbot</em>. Справка по его загрузке и установке есть на <a href="https://www.torproject.org/docs/android.html.en">Сайте Проекта Tor</a>.</p>
<p>We also have experimental packages for <a href="https://www.torproject.org/docs/N900.html.en">Nokia Maemo/N900</a> and <a href="http://sid77.slackware.it/ios/">Apple iOS</a>.</p>
<h3 id="how-to-verify-that-you-have-the-right-version">How to verify that you have the right version</h3>
<p>Before running the Tor Browser Bundle, you should make sure that you have the right version.</p>
1
0
commit 35307a623fd3de146ef11e85ae564b3508f6b87b
Author: Translation commit bot <translation(a)torproject.org>
Date: Tue May 29 10:45:09 2012 +0000
Update translations for tsum
---
ru/short-user-manual_ru_noimg.xhtml | 2 +-
1 files changed, 1 insertions(+), 1 deletions(-)
diff --git a/ru/short-user-manual_ru_noimg.xhtml b/ru/short-user-manual_ru_noimg.xhtml
index 5cba14a..9a8817f 100644
--- a/ru/short-user-manual_ru_noimg.xhtml
+++ b/ru/short-user-manual_ru_noimg.xhtml
@@ -13,7 +13,7 @@
<p>Всё, что вы посылаете в Интернет через Tor, становится, во-первых анонимным (скрывается источник), а во-вторых, остаётся зашифрованным на всём пути между Вашим компьютером и последним ретранслятором. Но после того, как данные покидают последний ретранслятор и отправляются к интернет-адресу своего назначения - они идут уже не в зашифрованном, а обычном, открытом виде.</p>
<p>Если вы передаёте особо важные данные - например, вводите логин и пароль для входа на веб-сайт - убедитесь, что работает протокол HTTPS (т.е. в адресной строке написано, например, <strong>https</strong>://torproject.org/, а не <strong>http</strong>://torproject.org/).</p>
<h2 id="how-to-download-tor">Как загрузить Tor</h2>
- <p>The bundle we recommend to most users is the <a href="https://www.torproject.org/projects/torbrowser.html">Tor Browser Bundle</a>. This bundle contains a browser preconfigured to safely browse the Internet through Tor, and requires no installation. You download the bundle, extract the archive, and start Tor.</p>
+ <p>Пакет программ, который мы обычно рекомендуем - это <a href="https://www.torproject.org/projects/torbrowser.html">Tor Browser Bundle</a>. Этот пакет не требует установки и содержит браузер, заранее настроенный таким образом, чтобы безопасно выходить в интернет через Tor. Вы просто загружаете пакет, распаковываете архив и запускаете Tor.</p>
<p>There are two different ways to get hold of the Tor software. You can either browse to the <a href="https://www.torproject.org/">Tor Project website</a> and download it there, or you can use GetTor, the email autoresponder.</p>
<h3 id="how-to-get-tor-via-email">How to get Tor via email</h3>
<p>To receive the English Tor Browser Bundle for Windows, send an email to gettor(a)torproject.org with <strong>windows</strong> in the body of the message. You can leave the subject blank.</p>
1
0
commit 72aed261dceb560d05563fab1af4e6f549e2bdfd
Author: Damian Johnson <atagar(a)torproject.org>
Date: Mon May 28 15:23:52 2012 -0700
Single retry for socket connection
I just had integ tests fail due to an interrupt while trying to connect to the
tor control socket. This is the first time that I've seen it, so this isn't
much of a concern, but connecting to a socket is idempotent so we can do with
retrying it once if we fail.
---
stem/socket.py | 10 +++++++++-
1 files changed, 9 insertions(+), 1 deletions(-)
diff --git a/stem/socket.py b/stem/socket.py
index 6f0bfda..5abe902 100644
--- a/stem/socket.py
+++ b/stem/socket.py
@@ -206,7 +206,15 @@ class ControlSocket:
self._socket_file = self._socket.makefile()
self._is_alive = True
- self._connect()
+ # It's possable for this to have a transient failure...
+ # SocketError: [Errno 4] Interrupted system call
+ #
+ # It's safe to retry, so give it another try if it fails.
+
+ try:
+ self._connect()
+ except SocketError:
+ self._connect() # single retry
def close(self):
"""
1
0
28 May '12
commit 152059f7d687c93326e039c954bbfa72a9356323
Author: Damian Johnson <atagar(a)torproject.org>
Date: Mon May 28 15:43:35 2012 -0700
Adding an is_ok() method to controller responses
Method added by Ravi during his safecookie work. Stealing it a little early
since I want it for refactoring that I'm doing.
---
stem/socket.py | 13 +++++++++++++
1 files changed, 13 insertions(+), 0 deletions(-)
diff --git a/stem/socket.py b/stem/socket.py
index 5abe902..a9e2068 100644
--- a/stem/socket.py
+++ b/stem/socket.py
@@ -408,6 +408,19 @@ class ControlMessage:
self._parsed_content = parsed_content
self._raw_content = raw_content
+ def is_ok(self):
+ """
+ Checks if all of our lines have a 250 response.
+
+ Returns:
+ True if all lines have a 250 response code, False otherwise
+ """
+
+ for code, _, _ in self._parsed_content:
+ if code != "250": return False
+
+ return True
+
def content(self):
"""
Provides the parsed message content. These are entries of the form...
1
0
28 May '12
commit 1d7ab654ec473a5e54ea3253f82abdcadc93158f
Author: Damian Johnson <atagar(a)torproject.org>
Date: Mon May 28 15:12:16 2012 -0700
Moving tor response classes into their own module
As we add more response classes it'll be messy to sprinkle them all about the
codebase. Making a single 'stem.response' module that'll contain them all.
These could probably do with some more love so I'll next see if I can make them
any tidier.
---
run_tests.py | 12 +-
stem/__init__.py | 2 +-
stem/connection.py | 164 +-----------------------------
stem/control.py | 67 +------------
stem/response/__init__.py | 46 +++++++++
stem/response/getinfo.py | 43 ++++++++
stem/response/protocolinfo.py | 131 ++++++++++++++++++++++++
test/integ/connection/__init__.py | 2 +-
test/integ/connection/protocolinfo.py | 138 -------------------------
test/integ/response/__init__.py | 6 +
test/integ/response/protocolinfo.py | 138 +++++++++++++++++++++++++
test/mocking.py | 8 +-
test/unit/connection/__init__.py | 2 +-
test/unit/connection/protocolinfo.py | 178 --------------------------------
test/unit/control/getinfo.py | 119 ----------------------
test/unit/response/__init__.py | 6 +
test/unit/response/getinfo.py | 120 ++++++++++++++++++++++
test/unit/response/protocolinfo.py | 180 +++++++++++++++++++++++++++++++++
18 files changed, 689 insertions(+), 673 deletions(-)
diff --git a/run_tests.py b/run_tests.py
index 13fb027..dea86c3 100755
--- a/run_tests.py
+++ b/run_tests.py
@@ -16,13 +16,13 @@ import test.output
import test.runner
import test.check_whitespace
import test.unit.connection.authentication
-import test.unit.connection.protocolinfo
-import test.unit.control.getinfo
import test.unit.socket.control_line
import test.unit.socket.control_message
import test.unit.descriptor.reader
import test.unit.descriptor.server_descriptor
import test.unit.descriptor.extrainfo_descriptor
+import test.unit.response.getinfo
+import test.unit.response.protocolinfo
import test.unit.util.conf
import test.unit.util.connection
import test.unit.util.enum
@@ -31,7 +31,6 @@ import test.unit.util.tor_tools
import test.unit.version
import test.integ.connection.authentication
import test.integ.connection.connect
-import test.integ.connection.protocolinfo
import test.integ.control.base_controller
import test.integ.control.controller
import test.integ.socket.control_message
@@ -39,6 +38,7 @@ import test.integ.socket.control_socket
import test.integ.descriptor.reader
import test.integ.descriptor.server_descriptor
import test.integ.descriptor.extrainfo_descriptor
+import test.integ.response.protocolinfo
import test.integ.util.conf
import test.integ.util.system
import test.integ.process
@@ -100,11 +100,11 @@ UNIT_TESTS = (
test.unit.descriptor.server_descriptor.TestServerDescriptor,
test.unit.descriptor.extrainfo_descriptor.TestExtraInfoDescriptor,
test.unit.version.TestVersion,
+ test.unit.response.getinfo.TestGetInfoResponse,
+ test.unit.response.protocolinfo.TestProtocolInfoResponse,
test.unit.socket.control_message.TestControlMessage,
test.unit.socket.control_line.TestControlLine,
test.unit.connection.authentication.TestAuthenticate,
- test.unit.connection.protocolinfo.TestProtocolInfoResponse,
- test.unit.control.getinfo.TestGetInfoResponse,
)
INTEG_TESTS = (
@@ -114,10 +114,10 @@ INTEG_TESTS = (
test.integ.descriptor.server_descriptor.TestServerDescriptor,
test.integ.descriptor.extrainfo_descriptor.TestExtraInfoDescriptor,
test.integ.version.TestVersion,
+ test.integ.response.protocolinfo.TestProtocolInfo,
test.integ.process.TestProcess,
test.integ.socket.control_socket.TestControlSocket,
test.integ.socket.control_message.TestControlMessage,
- test.integ.connection.protocolinfo.TestProtocolInfo,
test.integ.connection.authentication.TestAuthenticate,
test.integ.connection.connect.TestConnect,
test.integ.control.base_controller.TestBaseController,
diff --git a/stem/__init__.py b/stem/__init__.py
index ceb137f..30e4f81 100644
--- a/stem/__init__.py
+++ b/stem/__init__.py
@@ -2,5 +2,5 @@
Library for working with the tor process.
"""
-__all__ = ["descriptor", "util", "connection", "control", "process", "socket", "version"]
+__all__ = ["descriptor", "response", "util", "connection", "control", "process", "socket", "version"]
diff --git a/stem/connection.py b/stem/connection.py
index 983eb84..34c9c68 100644
--- a/stem/connection.py
+++ b/stem/connection.py
@@ -48,14 +48,6 @@ authenticate_password - Authenticates to a socket supporting password auth.
authenticate_cookie - Authenticates to a socket supporting cookie auth.
get_protocolinfo - Issues a PROTOCOLINFO query.
-ProtocolInfoResponse - Reply from a PROTOCOLINFO query.
- |- Attributes:
- | |- protocol_version
- | |- tor_version
- | |- auth_methods
- | |- unknown_auth_methods
- | +- cookie_path
- +- convert - parses a ControlMessage, turning it into a ProtocolInfoResponse
AuthenticationFailure - Base exception raised for authentication failures.
|- UnrecognizedAuthMethods - Authentication methods are unsupported.
@@ -84,6 +76,7 @@ import os
import getpass
import binascii
+import stem.response
import stem.socket
import stem.version
import stem.util.enum
@@ -295,7 +288,7 @@ def authenticate(controller, password = None, chroot_path = None, protocolinfo_r
password (str) - passphrase to present to the socket if it uses password
authentication (skips password auth if None)
chroot_path (str) - path prefix if in a chroot environment
- protocolinfo_response (stem.connection.ProtocolInfoResponse) -
+ protocolinfo_response (stem.response.protocolinfo.ProtocolInfoResponse) -
tor protocolinfo response, this is retrieved on our own if None
Raises:
@@ -640,7 +633,7 @@ def get_protocolinfo(controller):
tor controller connection
Returns:
- stem.connection.ProtocolInfoResponse provided by tor
+ stem.response.protocolinfo.ProtocolInfoResponse provided by tor
Raises:
stem.socket.ProtocolError if the PROTOCOLINFO response is malformed
@@ -664,7 +657,7 @@ def get_protocolinfo(controller):
except stem.socket.SocketClosed, exc:
raise stem.socket.SocketError(exc)
- ProtocolInfoResponse.convert(protocolinfo_response)
+ stem.response.convert("PROTOCOLINFO", protocolinfo_response)
# attempt to expand relative cookie paths via the control port or socket file
@@ -723,152 +716,3 @@ def _expand_cookie_path(protocolinfo_response, pid_resolver, pid_resolution_arg)
protocolinfo_response.cookie_path = cookie_path
-class ProtocolInfoResponse(stem.socket.ControlMessage):
- """
- Version one PROTOCOLINFO query response.
-
- According to the control spec the cookie_file is an absolute path. However,
- this often is not the case (especially for the Tor Browser Bundle)...
- https://trac.torproject.org/projects/tor/ticket/1101
-
- If the path is relative then we'll make an attempt (which may not work) to
- correct this.
-
- The protocol_version is the only mandatory data for a valid PROTOCOLINFO
- response, so all other values are None if undefined or empty if a collection.
-
- Attributes:
- protocol_version (int) - protocol version of the response
- tor_version (stem.version.Version) - version of the tor process
- auth_methods (tuple) - AuthMethod types that tor will accept
- unknown_auth_methods (tuple) - strings of unrecognized auth methods
- cookie_path (str) - path of tor's authentication cookie
- """
-
- def convert(control_message):
- """
- Parses a ControlMessage, performing an in-place conversion of it into a
- ProtocolInfoResponse.
-
- Arguments:
- control_message (stem.socket.ControlMessage) -
- message to be parsed as a PROTOCOLINFO reply
-
- Raises:
- stem.socket.ProtocolError the message isn't a proper PROTOCOLINFO response
- TypeError if argument isn't a ControlMessage
- """
-
- if isinstance(control_message, stem.socket.ControlMessage):
- control_message.__class__ = ProtocolInfoResponse
- control_message._parse_message()
- return control_message
- else:
- raise TypeError("Only able to convert stem.socket.ControlMessage instances")
-
- convert = staticmethod(convert)
-
- def _parse_message(self):
- # Example:
- # 250-PROTOCOLINFO 1
- # 250-AUTH METHODS=COOKIE COOKIEFILE="/home/atagar/.tor/control_auth_cookie"
- # 250-VERSION Tor="0.2.1.30"
- # 250 OK
-
- self.protocol_version = None
- self.tor_version = None
- self.cookie_path = None
-
- auth_methods, unknown_auth_methods = [], []
-
- # sanity check that we're a PROTOCOLINFO response
- if not list(self)[0].startswith("PROTOCOLINFO"):
- msg = "Message is not a PROTOCOLINFO response (%s)" % self
- raise stem.socket.ProtocolError(msg)
-
- for line in self:
- if line == "OK": break
- elif line.is_empty(): continue # blank line
-
- line_type = line.pop()
-
- if line_type == "PROTOCOLINFO":
- # Line format:
- # FirstLine = "PROTOCOLINFO" SP PIVERSION CRLF
- # PIVERSION = 1*DIGIT
-
- if line.is_empty():
- msg = "PROTOCOLINFO response's initial line is missing the protocol version: %s" % line
- raise stem.socket.ProtocolError(msg)
-
- piversion = line.pop()
-
- if not piversion.isdigit():
- msg = "PROTOCOLINFO response version is non-numeric: %s" % line
- raise stem.socket.ProtocolError(msg)
-
- self.protocol_version = int(piversion)
-
- # The piversion really should be "1" but, according to the spec, tor
- # does not necessarily need to provide the PROTOCOLINFO version that we
- # requested. Log if it's something we aren't expecting but still make
- # an effort to parse like a v1 response.
-
- if self.protocol_version != 1:
- log.info("We made a PROTOCOLINFO version 1 query but got a version %i response instead. We'll still try to use it, but this may cause problems." % self.protocol_version)
- elif line_type == "AUTH":
- # Line format:
- # AuthLine = "250-AUTH" SP "METHODS=" AuthMethod *("," AuthMethod)
- # *(SP "COOKIEFILE=" AuthCookieFile) CRLF
- # AuthMethod = "NULL" / "HASHEDPASSWORD" / "COOKIE"
- # AuthCookieFile = QuotedString
-
- # parse AuthMethod mapping
- if not line.is_next_mapping("METHODS"):
- msg = "PROTOCOLINFO response's AUTH line is missing its mandatory 'METHODS' mapping: %s" % line
- raise stem.socket.ProtocolError(msg)
-
- for method in line.pop_mapping()[1].split(","):
- if method == "NULL":
- auth_methods.append(AuthMethod.NONE)
- elif method == "HASHEDPASSWORD":
- auth_methods.append(AuthMethod.PASSWORD)
- elif method == "COOKIE":
- auth_methods.append(AuthMethod.COOKIE)
- else:
- unknown_auth_methods.append(method)
- message_id = "stem.connection.unknown_auth_%s" % method
- log.log_once(message_id, log.INFO, "PROTOCOLINFO response included a type of authentication that we don't recognize: %s" % method)
-
- # our auth_methods should have a single AuthMethod.UNKNOWN entry if
- # any unknown authentication methods exist
- if not AuthMethod.UNKNOWN in auth_methods:
- auth_methods.append(AuthMethod.UNKNOWN)
-
- # parse optional COOKIEFILE mapping (quoted and can have escapes)
- if line.is_next_mapping("COOKIEFILE", True, True):
- self.cookie_path = line.pop_mapping(True, True)[1]
-
- # attempt to expand relative cookie paths
- _expand_cookie_path(self, stem.util.system.get_pid_by_name, "tor")
- elif line_type == "VERSION":
- # Line format:
- # VersionLine = "250-VERSION" SP "Tor=" TorVersion OptArguments CRLF
- # TorVersion = QuotedString
-
- if not line.is_next_mapping("Tor", True):
- msg = "PROTOCOLINFO response's VERSION line is missing its mandatory tor version mapping: %s" % line
- raise stem.socket.ProtocolError(msg)
-
- torversion = line.pop_mapping(True)[1]
-
- try:
- self.tor_version = stem.version.Version(torversion)
- except ValueError, exc:
- raise stem.socket.ProtocolError(exc)
- else:
- log.debug("unrecognized PROTOCOLINFO line type '%s', ignoring entry: %s" % (line_type, line))
-
- self.auth_methods = tuple(auth_methods)
- self.unknown_auth_methods = tuple(unknown_auth_methods)
-
diff --git a/stem/control.py b/stem/control.py
index ccb57f4..b188ea4 100644
--- a/stem/control.py
+++ b/stem/control.py
@@ -26,6 +26,7 @@ import time
import Queue
import threading
+import stem.response
import stem.socket
import stem.util.log as log
@@ -475,7 +476,7 @@ class Controller(BaseController):
if response.content()[0][0] != "250":
raise stem.socket.ControllerError(str(response))
- GetInfoResponse.convert(response)
+ stem.response.convert("GETINFO", response)
# error if we got back different parameters than we requested
requested_params = set(param)
@@ -495,67 +496,3 @@ class Controller(BaseController):
if default == UNDEFINED: raise exc
else: return default
-class GetInfoResponse(stem.socket.ControlMessage):
- """
- Reply for a GETINFO query.
-
- Attributes:
- values (dict) - mapping between the queried options and their values
- """
-
- def convert(control_message):
- """
- Parses a ControlMessage, performing an in-place conversion of it into a
- GetInfoResponse.
-
- Arguments:
- control_message (stem.socket.ControlMessage) -
- message to be parsed as a GETINFO reply
-
- Raises:
- stem.socket.ProtocolError the message isn't a proper GETINFO response
- TypeError if argument isn't a ControlMessage
- """
-
- if isinstance(control_message, stem.socket.ControlMessage):
- control_message.__class__ = GetInfoResponse
- control_message._parse_message()
- return control_message
- else:
- raise TypeError("Only able to convert stem.socket.ControlMessage instances")
-
- convert = staticmethod(convert)
-
- def _parse_message(self):
- # Example:
- # 250-version=0.2.3.11-alpha-dev (git-ef0bc7f8f26a917c)
- # 250+config-text=
- # ControlPort 9051
- # DataDirectory /home/atagar/.tor
- # ExitPolicy reject *:*
- # Log notice stdout
- # Nickname Unnamed
- # ORPort 9050
- # .
- # 250 OK
-
- self.values = {}
-
- for line in self:
- if line == "OK": break
- elif not "=" in line:
- raise stem.socket.ProtocolError("GETINFO replies should only contain parameter=value mappings: %s" % line)
-
- key, value = line.split("=", 1)
-
- # if the value is a multiline value then it *must* be of the form
- # '<key>=\n<value>'
-
- if "\n" in value:
- if value.startswith("\n"):
- value = value[1:]
- else:
- raise stem.socket.ProtocolError("GETINFO response contained a multiline value that didn't start with a newline: %s" % line)
-
- self.values[key] = value
-
diff --git a/stem/response/__init__.py b/stem/response/__init__.py
new file mode 100644
index 0000000..92b9846
--- /dev/null
+++ b/stem/response/__init__.py
@@ -0,0 +1,46 @@
+"""
+Parses replies from the control socket.
+
+converts - translates a ControlMessage into a particular response subclass
+"""
+
+__all__ = ["getinfo", "protocolinfo"]
+
+import stem.socket
+
+def convert(response_type, message):
+ """
+ Converts a ControlMessage into a particular kind of tor response. This does
+ an in-place conversion of the message from being a ControlMessage to a
+ subclass for its response type. Recognized types include...
+
+ * GETINFO
+ * PROTOCOLINFO
+
+ If the response_type isn't recognized then this is leaves it alone.
+
+ Arguments:
+ response_type (str) - type of tor response to convert to
+ message (stem.socket.ControlMessage) - message to be converted
+
+ Raises:
+ stem.socket.ProtocolError the message isn't a proper response of that type
+ TypeError if argument isn't a ControlMessage or response_type isn't
+ supported
+ """
+
+ import stem.response.getinfo
+ import stem.response.protocolinfo
+
+ if not isinstance(message, stem.socket.ControlMessage):
+ raise TypeError("Only able to convert stem.socket.ControlMessage instances")
+
+ if response_type == "GETINFO":
+ response_class = stem.response.getinfo.GetInfoResponse
+ elif response_type == "PROTOCOLINFO":
+ response_class = stem.response.protocolinfo.ProtocolInfoResponse
+ else: raise TypeError("Unsupported response type: %s" % response_type)
+
+ message.__class__ = response_class
+ message._parse_message()
+
diff --git a/stem/response/getinfo.py b/stem/response/getinfo.py
new file mode 100644
index 0000000..a13a18f
--- /dev/null
+++ b/stem/response/getinfo.py
@@ -0,0 +1,43 @@
+import stem.socket
+
+class GetInfoResponse(stem.socket.ControlMessage):
+ """
+ Reply for a GETINFO query.
+
+ Attributes:
+ values (dict) - mapping between the queried options and their values
+ """
+
+ def _parse_message(self):
+ # Example:
+ # 250-version=0.2.3.11-alpha-dev (git-ef0bc7f8f26a917c)
+ # 250+config-text=
+ # ControlPort 9051
+ # DataDirectory /home/atagar/.tor
+ # ExitPolicy reject *:*
+ # Log notice stdout
+ # Nickname Unnamed
+ # ORPort 9050
+ # .
+ # 250 OK
+
+ self.values = {}
+
+ for line in self:
+ if line == "OK": break
+ elif not "=" in line:
+ raise stem.socket.ProtocolError("GETINFO replies should only contain parameter=value mappings: %s" % line)
+
+ key, value = line.split("=", 1)
+
+ # if the value is a multiline value then it *must* be of the form
+ # '<key>=\n<value>'
+
+ if "\n" in value:
+ if value.startswith("\n"):
+ value = value[1:]
+ else:
+ raise stem.socket.ProtocolError("GETINFO response contained a multiline value that didn't start with a newline: %s" % line)
+
+ self.values[key] = value
+
diff --git a/stem/response/protocolinfo.py b/stem/response/protocolinfo.py
new file mode 100644
index 0000000..4547a1d
--- /dev/null
+++ b/stem/response/protocolinfo.py
@@ -0,0 +1,131 @@
+import stem.connection
+import stem.socket
+import stem.version
+import stem.util.log as log
+
+class ProtocolInfoResponse(stem.socket.ControlMessage):
+ """
+ Version one PROTOCOLINFO query response.
+
+ According to the control spec the cookie_file is an absolute path. However,
+ this often is not the case (especially for the Tor Browser Bundle)...
+ https://trac.torproject.org/projects/tor/ticket/1101
+
+ If the path is relative then we'll make an attempt (which may not work) to
+ correct this.
+
+ The protocol_version is the only mandatory data for a valid PROTOCOLINFO
+ response, so all other values are None if undefined or empty if a collection.
+
+ Attributes:
+ protocol_version (int) - protocol version of the response
+ tor_version (stem.version.Version) - version of the tor process
+ auth_methods (tuple) - AuthMethod types that tor will accept
+ unknown_auth_methods (tuple) - strings of unrecognized auth methods
+ cookie_path (str) - path of tor's authentication cookie
+ """
+
+ def _parse_message(self):
+ # Example:
+ # 250-PROTOCOLINFO 1
+ # 250-AUTH METHODS=COOKIE COOKIEFILE="/home/atagar/.tor/control_auth_cookie"
+ # 250-VERSION Tor="0.2.1.30"
+ # 250 OK
+
+ self.protocol_version = None
+ self.tor_version = None
+ self.cookie_path = None
+
+ auth_methods, unknown_auth_methods = [], []
+
+ # sanity check that we're a PROTOCOLINFO response
+ if not list(self)[0].startswith("PROTOCOLINFO"):
+ msg = "Message is not a PROTOCOLINFO response (%s)" % self
+ raise stem.socket.ProtocolError(msg)
+
+ for line in self:
+ if line == "OK": break
+ elif line.is_empty(): continue # blank line
+
+ line_type = line.pop()
+
+ if line_type == "PROTOCOLINFO":
+ # Line format:
+ # FirstLine = "PROTOCOLINFO" SP PIVERSION CRLF
+ # PIVERSION = 1*DIGIT
+
+ if line.is_empty():
+ msg = "PROTOCOLINFO response's initial line is missing the protocol version: %s" % line
+ raise stem.socket.ProtocolError(msg)
+
+ piversion = line.pop()
+
+ if not piversion.isdigit():
+ msg = "PROTOCOLINFO response version is non-numeric: %s" % line
+ raise stem.socket.ProtocolError(msg)
+
+ self.protocol_version = int(piversion)
+
+ # The piversion really should be "1" but, according to the spec, tor
+ # does not necessarily need to provide the PROTOCOLINFO version that we
+ # requested. Log if it's something we aren't expecting but still make
+ # an effort to parse like a v1 response.
+
+ if self.protocol_version != 1:
+ log.info("We made a PROTOCOLINFO version 1 query but got a version %i response instead. We'll still try to use it, but this may cause problems." % self.protocol_version)
+ elif line_type == "AUTH":
+ # Line format:
+ # AuthLine = "250-AUTH" SP "METHODS=" AuthMethod *("," AuthMethod)
+ # *(SP "COOKIEFILE=" AuthCookieFile) CRLF
+ # AuthMethod = "NULL" / "HASHEDPASSWORD" / "COOKIE"
+ # AuthCookieFile = QuotedString
+
+ # parse AuthMethod mapping
+ if not line.is_next_mapping("METHODS"):
+ msg = "PROTOCOLINFO response's AUTH line is missing its mandatory 'METHODS' mapping: %s" % line
+ raise stem.socket.ProtocolError(msg)
+
+ for method in line.pop_mapping()[1].split(","):
+ if method == "NULL":
+ auth_methods.append(stem.connection.AuthMethod.NONE)
+ elif method == "HASHEDPASSWORD":
+ auth_methods.append(stem.connection.AuthMethod.PASSWORD)
+ elif method == "COOKIE":
+ auth_methods.append(stem.connection.AuthMethod.COOKIE)
+ else:
+ unknown_auth_methods.append(method)
+ message_id = "stem.connection.unknown_auth_%s" % method
+ log.log_once(message_id, log.INFO, "PROTOCOLINFO response included a type of authentication that we don't recognize: %s" % method)
+
+ # our auth_methods should have a single AuthMethod.UNKNOWN entry if
+ # any unknown authentication methods exist
+ if not stem.connection.AuthMethod.UNKNOWN in auth_methods:
+ auth_methods.append(stem.connection.AuthMethod.UNKNOWN)
+
+ # parse optional COOKIEFILE mapping (quoted and can have escapes)
+ if line.is_next_mapping("COOKIEFILE", True, True):
+ self.cookie_path = line.pop_mapping(True, True)[1]
+
+ # attempt to expand relative cookie paths
+ stem.connection._expand_cookie_path(self, stem.util.system.get_pid_by_name, "tor")
+ elif line_type == "VERSION":
+ # Line format:
+ # VersionLine = "250-VERSION" SP "Tor=" TorVersion OptArguments CRLF
+ # TorVersion = QuotedString
+
+ if not line.is_next_mapping("Tor", True):
+ msg = "PROTOCOLINFO response's VERSION line is missing its mandatory tor version mapping: %s" % line
+ raise stem.socket.ProtocolError(msg)
+
+ torversion = line.pop_mapping(True)[1]
+
+ try:
+ self.tor_version = stem.version.Version(torversion)
+ except ValueError, exc:
+ raise stem.socket.ProtocolError(exc)
+ else:
+ log.debug("unrecognized PROTOCOLINFO line type '%s', ignoring entry: %s" % (line_type, line))
+
+ self.auth_methods = tuple(auth_methods)
+ self.unknown_auth_methods = tuple(unknown_auth_methods)
+
diff --git a/test/integ/connection/__init__.py b/test/integ/connection/__init__.py
index 90471c0..eec60e6 100644
--- a/test/integ/connection/__init__.py
+++ b/test/integ/connection/__init__.py
@@ -2,5 +2,5 @@
Integration tests for stem.connection.
"""
-__all__ = ["authenticate", "connect", "protocolinfo"]
+__all__ = ["authenticate", "connect"]
diff --git a/test/integ/connection/protocolinfo.py b/test/integ/connection/protocolinfo.py
deleted file mode 100644
index 728c746..0000000
--- a/test/integ/connection/protocolinfo.py
+++ /dev/null
@@ -1,138 +0,0 @@
-"""
-Integration tests for the stem.connection.ProtocolInfoResponse class and
-related functions.
-"""
-
-import unittest
-
-import test.runner
-import stem.socket
-import stem.connection
-import stem.util.system
-import test.mocking as mocking
-from test.integ.util.system import filter_system_call
-
-class TestProtocolInfo(unittest.TestCase):
- def setUp(self):
- test.runner.require_control(self)
- mocking.mock(stem.util.proc.is_available, mocking.return_false())
- mocking.mock(stem.util.system.is_available, mocking.return_true())
-
- def tearDown(self):
- mocking.revert_mocking()
-
- def test_parsing(self):
- """
- Makes a PROTOCOLINFO query and processes the response for our control
- connection.
- """
-
- control_socket = test.runner.get_runner().get_tor_socket(False)
- control_socket.send("PROTOCOLINFO 1")
- protocolinfo_response = control_socket.recv()
- stem.connection.ProtocolInfoResponse.convert(protocolinfo_response)
- control_socket.close()
-
- # according to the control spec the following _could_ differ or be
- # undefined but if that actually happens then it's gonna make people sad
-
- self.assertEqual(1, protocolinfo_response.protocol_version)
- self.assertNotEqual(None, protocolinfo_response.tor_version)
- self.assertNotEqual(None, protocolinfo_response.auth_methods)
-
- self.assert_matches_test_config(protocolinfo_response)
-
- def test_get_protocolinfo_path_expansion(self):
- """
- If we're running with the 'RELATIVE' target then test_parsing() will
- exercise cookie path expansion when we're able to query the pid by our
- prcess name. This test selectively disables system.call() so we exercise
- the expansion via our control port or socket file.
-
- This test is largely redundant with test_parsing() if we aren't running
- with the 'RELATIVE' target.
- """
-
- if test.runner.Torrc.PORT in test.runner.get_runner().get_options():
- cwd_by_port_lookup_prefixes = (
- stem.util.system.GET_PID_BY_PORT_NETSTAT,
- stem.util.system.GET_PID_BY_PORT_SOCKSTAT % "",
- stem.util.system.GET_PID_BY_PORT_LSOF,
- stem.util.system.GET_CWD_PWDX % "",
- "lsof -a -p ")
-
- mocking.mock(stem.util.system.call, filter_system_call(cwd_by_port_lookup_prefixes))
- control_socket = stem.socket.ControlPort(control_port = test.runner.CONTROL_PORT)
- else:
- cwd_by_socket_lookup_prefixes = (
- stem.util.system.GET_PID_BY_FILE_LSOF % "",
- stem.util.system.GET_CWD_PWDX % "",
- "lsof -a -p ")
-
- mocking.mock(stem.util.system.call, filter_system_call(cwd_by_socket_lookup_prefixes))
- control_socket = stem.socket.ControlSocketFile(test.runner.CONTROL_SOCKET_PATH)
-
- protocolinfo_response = stem.connection.get_protocolinfo(control_socket)
- self.assert_matches_test_config(protocolinfo_response)
-
- # we should have a usable socket at this point
- self.assertTrue(control_socket.is_alive())
- control_socket.close()
-
- def test_multiple_protocolinfo_calls(self):
- """
- Tests making repeated PROTOCOLINFO queries. This use case is interesting
- because tor will shut down the socket and stem should transparently
- re-establish it.
- """
-
- with test.runner.get_runner().get_tor_socket(False) as control_socket:
- for i in range(5):
- protocolinfo_response = stem.connection.get_protocolinfo(control_socket)
- self.assert_matches_test_config(protocolinfo_response)
-
- def test_pre_disconnected_query(self):
- """
- Tests making a PROTOCOLINFO query when previous use of the socket had
- already disconnected it.
- """
-
- with test.runner.get_runner().get_tor_socket(False) as control_socket:
- # makes a couple protocolinfo queries outside of get_protocolinfo first
- control_socket.send("PROTOCOLINFO 1")
- control_socket.recv()
-
- control_socket.send("PROTOCOLINFO 1")
- control_socket.recv()
-
- protocolinfo_response = stem.connection.get_protocolinfo(control_socket)
- self.assert_matches_test_config(protocolinfo_response)
-
- def assert_matches_test_config(self, protocolinfo_response):
- """
- Makes assertions that the protocolinfo response's attributes match those of
- the test configuration.
- """
-
- runner = test.runner.get_runner()
- tor_options = runner.get_options()
- auth_methods, auth_cookie_path = [], None
-
- if test.runner.Torrc.COOKIE in tor_options:
- auth_methods.append(stem.connection.AuthMethod.COOKIE)
- chroot_path = runner.get_chroot()
- auth_cookie_path = runner.get_auth_cookie_path()
-
- if chroot_path and auth_cookie_path.startswith(chroot_path):
- auth_cookie_path = auth_cookie_path[len(chroot_path):]
-
- if test.runner.Torrc.PASSWORD in tor_options:
- auth_methods.append(stem.connection.AuthMethod.PASSWORD)
-
- if not auth_methods:
- auth_methods.append(stem.connection.AuthMethod.NONE)
-
- self.assertEqual((), protocolinfo_response.unknown_auth_methods)
- self.assertEqual(tuple(auth_methods), protocolinfo_response.auth_methods)
- self.assertEqual(auth_cookie_path, protocolinfo_response.cookie_path)
-
diff --git a/test/integ/response/__init__.py b/test/integ/response/__init__.py
new file mode 100644
index 0000000..2e3a991
--- /dev/null
+++ b/test/integ/response/__init__.py
@@ -0,0 +1,6 @@
+"""
+Integration tests for stem.response.
+"""
+
+__all__ = ["protocolinfo"]
+
diff --git a/test/integ/response/protocolinfo.py b/test/integ/response/protocolinfo.py
new file mode 100644
index 0000000..f5eb518
--- /dev/null
+++ b/test/integ/response/protocolinfo.py
@@ -0,0 +1,138 @@
+"""
+Integration tests for the stem.response.protocolinfo.ProtocolInfoResponse class
+and related functions.
+"""
+
+import unittest
+
+import test.runner
+import stem.socket
+import stem.connection
+import stem.util.system
+import test.mocking as mocking
+from test.integ.util.system import filter_system_call
+
+class TestProtocolInfo(unittest.TestCase):
+ def setUp(self):
+ test.runner.require_control(self)
+ mocking.mock(stem.util.proc.is_available, mocking.return_false())
+ mocking.mock(stem.util.system.is_available, mocking.return_true())
+
+ def tearDown(self):
+ mocking.revert_mocking()
+
+ def test_parsing(self):
+ """
+ Makes a PROTOCOLINFO query and processes the response for our control
+ connection.
+ """
+
+ control_socket = test.runner.get_runner().get_tor_socket(False)
+ control_socket.send("PROTOCOLINFO 1")
+ protocolinfo_response = control_socket.recv()
+ stem.response.convert("PROTOCOLINFO", protocolinfo_response)
+ control_socket.close()
+
+ # according to the control spec the following _could_ differ or be
+ # undefined but if that actually happens then it's gonna make people sad
+
+ self.assertEqual(1, protocolinfo_response.protocol_version)
+ self.assertNotEqual(None, protocolinfo_response.tor_version)
+ self.assertNotEqual(None, protocolinfo_response.auth_methods)
+
+ self.assert_matches_test_config(protocolinfo_response)
+
+ def test_get_protocolinfo_path_expansion(self):
+ """
+ If we're running with the 'RELATIVE' target then test_parsing() will
+ exercise cookie path expansion when we're able to query the pid by our
+ prcess name. This test selectively disables system.call() so we exercise
+ the expansion via our control port or socket file.
+
+ This test is largely redundant with test_parsing() if we aren't running
+ with the 'RELATIVE' target.
+ """
+
+ if test.runner.Torrc.PORT in test.runner.get_runner().get_options():
+ cwd_by_port_lookup_prefixes = (
+ stem.util.system.GET_PID_BY_PORT_NETSTAT,
+ stem.util.system.GET_PID_BY_PORT_SOCKSTAT % "",
+ stem.util.system.GET_PID_BY_PORT_LSOF,
+ stem.util.system.GET_CWD_PWDX % "",
+ "lsof -a -p ")
+
+ mocking.mock(stem.util.system.call, filter_system_call(cwd_by_port_lookup_prefixes))
+ control_socket = stem.socket.ControlPort(control_port = test.runner.CONTROL_PORT)
+ else:
+ cwd_by_socket_lookup_prefixes = (
+ stem.util.system.GET_PID_BY_FILE_LSOF % "",
+ stem.util.system.GET_CWD_PWDX % "",
+ "lsof -a -p ")
+
+ mocking.mock(stem.util.system.call, filter_system_call(cwd_by_socket_lookup_prefixes))
+ control_socket = stem.socket.ControlSocketFile(test.runner.CONTROL_SOCKET_PATH)
+
+ protocolinfo_response = stem.connection.get_protocolinfo(control_socket)
+ self.assert_matches_test_config(protocolinfo_response)
+
+ # we should have a usable socket at this point
+ self.assertTrue(control_socket.is_alive())
+ control_socket.close()
+
+ def test_multiple_protocolinfo_calls(self):
+ """
+ Tests making repeated PROTOCOLINFO queries. This use case is interesting
+ because tor will shut down the socket and stem should transparently
+ re-establish it.
+ """
+
+ with test.runner.get_runner().get_tor_socket(False) as control_socket:
+ for i in range(5):
+ protocolinfo_response = stem.connection.get_protocolinfo(control_socket)
+ self.assert_matches_test_config(protocolinfo_response)
+
+ def test_pre_disconnected_query(self):
+ """
+ Tests making a PROTOCOLINFO query when previous use of the socket had
+ already disconnected it.
+ """
+
+ with test.runner.get_runner().get_tor_socket(False) as control_socket:
+ # makes a couple protocolinfo queries outside of get_protocolinfo first
+ control_socket.send("PROTOCOLINFO 1")
+ control_socket.recv()
+
+ control_socket.send("PROTOCOLINFO 1")
+ control_socket.recv()
+
+ protocolinfo_response = stem.connection.get_protocolinfo(control_socket)
+ self.assert_matches_test_config(protocolinfo_response)
+
+ def assert_matches_test_config(self, protocolinfo_response):
+ """
+ Makes assertions that the protocolinfo response's attributes match those of
+ the test configuration.
+ """
+
+ runner = test.runner.get_runner()
+ tor_options = runner.get_options()
+ auth_methods, auth_cookie_path = [], None
+
+ if test.runner.Torrc.COOKIE in tor_options:
+ auth_methods.append(stem.connection.AuthMethod.COOKIE)
+ chroot_path = runner.get_chroot()
+ auth_cookie_path = runner.get_auth_cookie_path()
+
+ if chroot_path and auth_cookie_path.startswith(chroot_path):
+ auth_cookie_path = auth_cookie_path[len(chroot_path):]
+
+ if test.runner.Torrc.PASSWORD in tor_options:
+ auth_methods.append(stem.connection.AuthMethod.PASSWORD)
+
+ if not auth_methods:
+ auth_methods.append(stem.connection.AuthMethod.NONE)
+
+ self.assertEqual((), protocolinfo_response.unknown_auth_methods)
+ self.assertEqual(tuple(auth_methods), protocolinfo_response.auth_methods)
+ self.assertEqual(auth_cookie_path, protocolinfo_response.cookie_path)
+
diff --git a/test/mocking.py b/test/mocking.py
index aa61c76..f17dcc9 100644
--- a/test/mocking.py
+++ b/test/mocking.py
@@ -18,7 +18,7 @@ Mocking Functions
Instance Constructors
get_message - stem.socket.ControlMessage
- get_protocolinfo_response - stem.connection.ProtocolInfoResponse
+ get_protocolinfo_response - stem.response.protocolinfo.ProtocolInfoResponse
"""
import inspect
@@ -26,7 +26,7 @@ import itertools
import StringIO
import __builtin__
-import stem.connection
+import stem.response
import stem.socket
# Once we've mocked a function we can't rely on its __module__ or __name__
@@ -223,11 +223,11 @@ def get_protocolinfo_response(**attributes):
attributes (dict) - attributes to customize the response with
Returns:
- stem.connection.ProtocolInfoResponse instance
+ stem.response.protocolinfo.ProtocolInfoResponse instance
"""
protocolinfo_response = get_message("250-PROTOCOLINFO 1\n250 OK")
- stem.connection.ProtocolInfoResponse.convert(protocolinfo_response)
+ stem.response.convert("PROTOCOLINFO", protocolinfo_response)
for attr in attributes:
protocolinfo_response.__dict__[attr] = attributes[attr]
diff --git a/test/unit/connection/__init__.py b/test/unit/connection/__init__.py
index 4eae0fa..7073319 100644
--- a/test/unit/connection/__init__.py
+++ b/test/unit/connection/__init__.py
@@ -2,5 +2,5 @@
Unit tests for stem.connection.
"""
-__all__ = ["authentication", "protocolinfo"]
+__all__ = ["authentication"]
diff --git a/test/unit/connection/protocolinfo.py b/test/unit/connection/protocolinfo.py
deleted file mode 100644
index 795d780..0000000
--- a/test/unit/connection/protocolinfo.py
+++ /dev/null
@@ -1,178 +0,0 @@
-"""
-Unit tests for the stem.connection.ProtocolInfoResponse class.
-"""
-
-import unittest
-
-import stem.connection
-import stem.socket
-import stem.version
-import stem.util.proc
-import stem.util.system
-import test.mocking as mocking
-
-NO_AUTH = """250-PROTOCOLINFO 1
-250-AUTH METHODS=NULL
-250-VERSION Tor="0.2.1.30"
-250 OK"""
-
-PASSWORD_AUTH = """250-PROTOCOLINFO 1
-250-AUTH METHODS=HASHEDPASSWORD
-250-VERSION Tor="0.2.1.30"
-250 OK"""
-
-COOKIE_AUTH = r"""250-PROTOCOLINFO 1
-250-AUTH METHODS=COOKIE COOKIEFILE="/tmp/my data\\\"dir//control_auth_cookie"
-250-VERSION Tor="0.2.1.30"
-250 OK"""
-
-MULTIPLE_AUTH = """250-PROTOCOLINFO 1
-250-AUTH METHODS=COOKIE,HASHEDPASSWORD COOKIEFILE="/home/atagar/.tor/control_auth_cookie"
-250-VERSION Tor="0.2.1.30"
-250 OK"""
-
-UNKNOWN_AUTH = """250-PROTOCOLINFO 1
-250-AUTH METHODS=MAGIC,HASHEDPASSWORD,PIXIE_DUST
-250-VERSION Tor="0.2.1.30"
-250 OK"""
-
-MINIMUM_RESPONSE = """250-PROTOCOLINFO 5
-250 OK"""
-
-RELATIVE_COOKIE_PATH = r"""250-PROTOCOLINFO 1
-250-AUTH METHODS=COOKIE COOKIEFILE="./tor-browser_en-US/Data/control_auth_cookie"
-250-VERSION Tor="0.2.1.30"
-250 OK"""
-
-class TestProtocolInfoResponse(unittest.TestCase):
- def test_convert(self):
- """
- Exercises functionality of the convert method both when it works and
- there's an error.
- """
-
- # working case
- control_message = mocking.get_message(NO_AUTH)
- stem.connection.ProtocolInfoResponse.convert(control_message)
-
- # now this should be a ProtocolInfoResponse (ControlMessage subclass)
- self.assertTrue(isinstance(control_message, stem.socket.ControlMessage))
- self.assertTrue(isinstance(control_message, stem.connection.ProtocolInfoResponse))
-
- # exercise some of the ControlMessage functionality
- raw_content = (NO_AUTH + "\n").replace("\n", "\r\n")
- self.assertEquals(raw_content, control_message.raw_content())
- self.assertTrue(str(control_message).startswith("PROTOCOLINFO 1"))
-
- # attempt to convert the wrong type
- self.assertRaises(TypeError, stem.connection.ProtocolInfoResponse.convert, "hello world")
-
- # attempt to convert a different message type
- bw_event_control_message = mocking.get_message("650 BW 32326 2856")
- self.assertRaises(stem.socket.ProtocolError, stem.connection.ProtocolInfoResponse.convert, bw_event_control_message)
-
- def test_no_auth(self):
- """
- Checks a response when there's no authentication.
- """
-
- control_message = mocking.get_message(NO_AUTH)
- stem.connection.ProtocolInfoResponse.convert(control_message)
-
- self.assertEquals(1, control_message.protocol_version)
- self.assertEquals(stem.version.Version("0.2.1.30"), control_message.tor_version)
- self.assertEquals((stem.connection.AuthMethod.NONE, ), control_message.auth_methods)
- self.assertEquals((), control_message.unknown_auth_methods)
- self.assertEquals(None, control_message.cookie_path)
-
- def test_password_auth(self):
- """
- Checks a response with password authentication.
- """
-
- control_message = mocking.get_message(PASSWORD_AUTH)
- stem.connection.ProtocolInfoResponse.convert(control_message)
- self.assertEquals((stem.connection.AuthMethod.PASSWORD, ), control_message.auth_methods)
-
- def test_cookie_auth(self):
- """
- Checks a response with cookie authentication and a path including escape
- characters.
- """
-
- control_message = mocking.get_message(COOKIE_AUTH)
- stem.connection.ProtocolInfoResponse.convert(control_message)
- self.assertEquals((stem.connection.AuthMethod.COOKIE, ), control_message.auth_methods)
- self.assertEquals("/tmp/my data\\\"dir//control_auth_cookie", control_message.cookie_path)
-
- def test_multiple_auth(self):
- """
- Checks a response with multiple authentication methods.
- """
-
- control_message = mocking.get_message(MULTIPLE_AUTH)
- stem.connection.ProtocolInfoResponse.convert(control_message)
- self.assertEquals((stem.connection.AuthMethod.COOKIE, stem.connection.AuthMethod.PASSWORD), control_message.auth_methods)
- self.assertEquals("/home/atagar/.tor/control_auth_cookie", control_message.cookie_path)
-
- def test_unknown_auth(self):
- """
- Checks a response with an unrecognized authtentication method.
- """
-
- control_message = mocking.get_message(UNKNOWN_AUTH)
- stem.connection.ProtocolInfoResponse.convert(control_message)
- self.assertEquals((stem.connection.AuthMethod.UNKNOWN, stem.connection.AuthMethod.PASSWORD), control_message.auth_methods)
- self.assertEquals(("MAGIC", "PIXIE_DUST"), control_message.unknown_auth_methods)
-
- def test_minimum_response(self):
- """
- Checks a PROTOCOLINFO response that only contains the minimum amount of
- information to be a valid response.
- """
-
- control_message = mocking.get_message(MINIMUM_RESPONSE)
- stem.connection.ProtocolInfoResponse.convert(control_message)
-
- self.assertEquals(5, control_message.protocol_version)
- self.assertEquals(None , control_message.tor_version)
- self.assertEquals((), control_message.auth_methods)
- self.assertEquals((), control_message.unknown_auth_methods)
- self.assertEquals(None, control_message.cookie_path)
-
- def test_relative_cookie(self):
- """
- Checks an authentication cookie with a relative path where expansion both
- succeeds and fails.
- """
-
- # we need to mock both pid and cwd lookups since the general cookie
- # expanion works by...
- # - resolving the pid of the "tor" process
- # - using that to get tor's cwd
-
- def call_mocking(command):
- if command == stem.util.system.GET_PID_BY_NAME_PGREP % "tor":
- return ["10"]
- elif command == stem.util.system.GET_CWD_PWDX % 10:
- return ["10: /tmp/foo"]
-
- mocking.mock(stem.util.proc.is_available, mocking.return_false())
- mocking.mock(stem.util.system.is_available, mocking.return_true())
- mocking.mock(stem.util.system.call, call_mocking)
-
- control_message = mocking.get_message(RELATIVE_COOKIE_PATH)
- stem.connection.ProtocolInfoResponse.convert(control_message)
- self.assertEquals("/tmp/foo/tor-browser_en-US/Data/control_auth_cookie", control_message.cookie_path)
-
- # exercise cookie expansion where both calls fail (should work, just
- # leaving the path unexpanded)
-
- mocking.mock(stem.util.system.call, mocking.return_none())
- control_message = mocking.get_message(RELATIVE_COOKIE_PATH)
- stem.connection.ProtocolInfoResponse.convert(control_message)
- self.assertEquals("./tor-browser_en-US/Data/control_auth_cookie", control_message.cookie_path)
-
- # reset system call mocking
- mocking.revert_mocking()
-
diff --git a/test/unit/control/getinfo.py b/test/unit/control/getinfo.py
deleted file mode 100644
index 3fc9fd3..0000000
--- a/test/unit/control/getinfo.py
+++ /dev/null
@@ -1,119 +0,0 @@
-"""
-Unit tests for the stem.control.GetInfoResponse class.
-"""
-
-import unittest
-
-import stem.connection
-import test.mocking as mocking
-
-EMPTY_RESPONSE = "250 OK"
-
-SINGLE_RESPONSE = """\
-250-version=0.2.3.11-alpha-dev
-250 OK"""
-
-BATCH_RESPONSE = """\
-250-version=0.2.3.11-alpha-dev
-250-address=67.137.76.214
-250-fingerprint=5FDE0422045DF0E1879A3738D09099EB4A0C5BA0
-250 OK"""
-
-MULTILINE_RESPONSE = """\
-250-version=0.2.3.11-alpha-dev (git-ef0bc7f8f26a917c)
-250+config-text=
-ControlPort 9051
-DataDirectory /home/atagar/.tor
-ExitPolicy reject *:*
-Log notice stdout
-Nickname Unnamed
-ORPort 9050
-.
-250 OK"""
-
-NON_KEY_VALUE_ENTRY = """\
-250-version=0.2.3.11-alpha-dev
-250-address 67.137.76.214
-250 OK"""
-
-MISSING_MULTILINE_NEWLINE = """\
-250+config-text=ControlPort 9051
-DataDirectory /home/atagar/.tor
-.
-250 OK"""
-
-class TestGetInfoResponse(unittest.TestCase):
- def test_empty_response(self):
- """
- Parses a GETINFO reply without options (just calling "GETINFO").
- """
-
- control_message = mocking.get_message(EMPTY_RESPONSE)
- stem.control.GetInfoResponse.convert(control_message)
-
- # now this should be a GetInfoResponse (ControlMessage subclass)
- self.assertTrue(isinstance(control_message, stem.socket.ControlMessage))
- self.assertTrue(isinstance(control_message, stem.control.GetInfoResponse))
-
- self.assertEqual({}, control_message.values)
-
- def test_single_response(self):
- """
- Parses a GETINFO reply response for a single parameter.
- """
-
- control_message = mocking.get_message(SINGLE_RESPONSE)
- stem.control.GetInfoResponse.convert(control_message)
- self.assertEqual({"version": "0.2.3.11-alpha-dev"}, control_message.values)
-
- def test_batch_response(self):
- """
- Parses a GETINFO reply for muiltiple parameters.
- """
-
- control_message = mocking.get_message(BATCH_RESPONSE)
- stem.control.GetInfoResponse.convert(control_message)
-
- expected = {
- "version": "0.2.3.11-alpha-dev",
- "address": "67.137.76.214",
- "fingerprint": "5FDE0422045DF0E1879A3738D09099EB4A0C5BA0",
- }
-
- self.assertEqual(expected, control_message.values)
-
- def test_multiline_response(self):
- """
- Parses a GETINFO reply for multiple parameters including a multi-line
- value.
- """
-
- control_message = mocking.get_message(MULTILINE_RESPONSE)
- stem.control.GetInfoResponse.convert(control_message)
-
- expected = {
- "version": "0.2.3.11-alpha-dev (git-ef0bc7f8f26a917c)",
- "config-text": "\n".join(MULTILINE_RESPONSE.splitlines()[2:8]),
- }
-
- self.assertEqual(expected, control_message.values)
-
- def test_invalid_non_mapping_content(self):
- """
- Parses a malformed GETINFO reply containing a line that isn't a key=value
- entry.
- """
-
- control_message = mocking.get_message(NON_KEY_VALUE_ENTRY)
- self.assertRaises(stem.socket.ProtocolError, stem.control.GetInfoResponse.convert, control_message)
-
- def test_invalid_multiline_content(self):
- """
- Parses a malformed GETINFO reply with a multi-line entry missing a newline
- between its key and value. This is a proper controller message, but
- malformed according to the GETINFO's spec.
- """
-
- control_message = mocking.get_message(MISSING_MULTILINE_NEWLINE)
- self.assertRaises(stem.socket.ProtocolError, stem.control.GetInfoResponse.convert, control_message)
-
diff --git a/test/unit/response/__init__.py b/test/unit/response/__init__.py
new file mode 100644
index 0000000..530274c
--- /dev/null
+++ b/test/unit/response/__init__.py
@@ -0,0 +1,6 @@
+"""
+Unit tests for stem.response.
+"""
+
+__all__ = ["getinfo", "protocolinfo"]
+
diff --git a/test/unit/response/getinfo.py b/test/unit/response/getinfo.py
new file mode 100644
index 0000000..5f5862a
--- /dev/null
+++ b/test/unit/response/getinfo.py
@@ -0,0 +1,120 @@
+"""
+Unit tests for the stem.response.getinfo.GetInfoResponse class.
+"""
+
+import unittest
+
+import stem.response
+import stem.response.getinfo
+import test.mocking as mocking
+
+EMPTY_RESPONSE = "250 OK"
+
+SINGLE_RESPONSE = """\
+250-version=0.2.3.11-alpha-dev
+250 OK"""
+
+BATCH_RESPONSE = """\
+250-version=0.2.3.11-alpha-dev
+250-address=67.137.76.214
+250-fingerprint=5FDE0422045DF0E1879A3738D09099EB4A0C5BA0
+250 OK"""
+
+MULTILINE_RESPONSE = """\
+250-version=0.2.3.11-alpha-dev (git-ef0bc7f8f26a917c)
+250+config-text=
+ControlPort 9051
+DataDirectory /home/atagar/.tor
+ExitPolicy reject *:*
+Log notice stdout
+Nickname Unnamed
+ORPort 9050
+.
+250 OK"""
+
+NON_KEY_VALUE_ENTRY = """\
+250-version=0.2.3.11-alpha-dev
+250-address 67.137.76.214
+250 OK"""
+
+MISSING_MULTILINE_NEWLINE = """\
+250+config-text=ControlPort 9051
+DataDirectory /home/atagar/.tor
+.
+250 OK"""
+
+class TestGetInfoResponse(unittest.TestCase):
+ def test_empty_response(self):
+ """
+ Parses a GETINFO reply without options (just calling "GETINFO").
+ """
+
+ control_message = mocking.get_message(EMPTY_RESPONSE)
+ stem.response.convert("GETINFO", control_message)
+
+ # now this should be a GetInfoResponse (ControlMessage subclass)
+ self.assertTrue(isinstance(control_message, stem.socket.ControlMessage))
+ self.assertTrue(isinstance(control_message, stem.response.getinfo.GetInfoResponse))
+
+ self.assertEqual({}, control_message.values)
+
+ def test_single_response(self):
+ """
+ Parses a GETINFO reply response for a single parameter.
+ """
+
+ control_message = mocking.get_message(SINGLE_RESPONSE)
+ stem.response.convert("GETINFO", control_message)
+ self.assertEqual({"version": "0.2.3.11-alpha-dev"}, control_message.values)
+
+ def test_batch_response(self):
+ """
+ Parses a GETINFO reply for muiltiple parameters.
+ """
+
+ control_message = mocking.get_message(BATCH_RESPONSE)
+ stem.response.convert("GETINFO", control_message)
+
+ expected = {
+ "version": "0.2.3.11-alpha-dev",
+ "address": "67.137.76.214",
+ "fingerprint": "5FDE0422045DF0E1879A3738D09099EB4A0C5BA0",
+ }
+
+ self.assertEqual(expected, control_message.values)
+
+ def test_multiline_response(self):
+ """
+ Parses a GETINFO reply for multiple parameters including a multi-line
+ value.
+ """
+
+ control_message = mocking.get_message(MULTILINE_RESPONSE)
+ stem.response.convert("GETINFO", control_message)
+
+ expected = {
+ "version": "0.2.3.11-alpha-dev (git-ef0bc7f8f26a917c)",
+ "config-text": "\n".join(MULTILINE_RESPONSE.splitlines()[2:8]),
+ }
+
+ self.assertEqual(expected, control_message.values)
+
+ def test_invalid_non_mapping_content(self):
+ """
+ Parses a malformed GETINFO reply containing a line that isn't a key=value
+ entry.
+ """
+
+ control_message = mocking.get_message(NON_KEY_VALUE_ENTRY)
+ self.assertRaises(stem.socket.ProtocolError, stem.response.convert, "GETINFO", control_message)
+
+ def test_invalid_multiline_content(self):
+ """
+ Parses a malformed GETINFO reply with a multi-line entry missing a newline
+ between its key and value. This is a proper controller message, but
+ malformed according to the GETINFO's spec.
+ """
+
+ control_message = mocking.get_message(MISSING_MULTILINE_NEWLINE)
+ self.assertRaises(stem.socket.ProtocolError, stem.response.convert, "GETINFO", control_message)
+
diff --git a/test/unit/response/protocolinfo.py b/test/unit/response/protocolinfo.py
new file mode 100644
index 0000000..a17c58c
--- /dev/null
+++ b/test/unit/response/protocolinfo.py
@@ -0,0 +1,180 @@
+"""
+Unit tests for the stem.response.protocolinfo.ProtocolInfoResponse class.
+"""
+
+import unittest
+
+from stem.connection import AuthMethod
+import stem.socket
+import stem.version
+import stem.util.proc
+import stem.util.system
+import stem.response
+import stem.response.protocolinfo
+import test.mocking as mocking
+
+NO_AUTH = """250-PROTOCOLINFO 1
+250-AUTH METHODS=NULL
+250-VERSION Tor="0.2.1.30"
+250 OK"""
+
+PASSWORD_AUTH = """250-PROTOCOLINFO 1
+250-AUTH METHODS=HASHEDPASSWORD
+250-VERSION Tor="0.2.1.30"
+250 OK"""
+
+COOKIE_AUTH = r"""250-PROTOCOLINFO 1
+250-AUTH METHODS=COOKIE COOKIEFILE="/tmp/my data\\\"dir//control_auth_cookie"
+250-VERSION Tor="0.2.1.30"
+250 OK"""
+
+MULTIPLE_AUTH = """250-PROTOCOLINFO 1
+250-AUTH METHODS=COOKIE,HASHEDPASSWORD COOKIEFILE="/home/atagar/.tor/control_auth_cookie"
+250-VERSION Tor="0.2.1.30"
+250 OK"""
+
+UNKNOWN_AUTH = """250-PROTOCOLINFO 1
+250-AUTH METHODS=MAGIC,HASHEDPASSWORD,PIXIE_DUST
+250-VERSION Tor="0.2.1.30"
+250 OK"""
+
+MINIMUM_RESPONSE = """250-PROTOCOLINFO 5
+250 OK"""
+
+RELATIVE_COOKIE_PATH = r"""250-PROTOCOLINFO 1
+250-AUTH METHODS=COOKIE COOKIEFILE="./tor-browser_en-US/Data/control_auth_cookie"
+250-VERSION Tor="0.2.1.30"
+250 OK"""
+
+class TestProtocolInfoResponse(unittest.TestCase):
+ def test_convert(self):
+ """
+ Exercises functionality of the convert method both when it works and
+ there's an error.
+ """
+
+ # working case
+ control_message = mocking.get_message(NO_AUTH)
+ stem.response.convert("PROTOCOLINFO", control_message)
+
+ # now this should be a ProtocolInfoResponse (ControlMessage subclass)
+ self.assertTrue(isinstance(control_message, stem.socket.ControlMessage))
+ self.assertTrue(isinstance(control_message, stem.response.protocolinfo.ProtocolInfoResponse))
+
+ # exercise some of the ControlMessage functionality
+ raw_content = (NO_AUTH + "\n").replace("\n", "\r\n")
+ self.assertEquals(raw_content, control_message.raw_content())
+ self.assertTrue(str(control_message).startswith("PROTOCOLINFO 1"))
+
+ # attempt to convert the wrong type
+ self.assertRaises(TypeError, stem.response.convert, "PROTOCOLINFO", "hello world")
+
+ # attempt to convert a different message type
+ bw_event_control_message = mocking.get_message("650 BW 32326 2856")
+ self.assertRaises(stem.socket.ProtocolError, stem.response.convert, "PROTOCOLINFO", bw_event_control_message)
+
+ def test_no_auth(self):
+ """
+ Checks a response when there's no authentication.
+ """
+
+ control_message = mocking.get_message(NO_AUTH)
+ stem.response.convert("PROTOCOLINFO", control_message)
+
+ self.assertEquals(1, control_message.protocol_version)
+ self.assertEquals(stem.version.Version("0.2.1.30"), control_message.tor_version)
+ self.assertEquals((AuthMethod.NONE, ), control_message.auth_methods)
+ self.assertEquals((), control_message.unknown_auth_methods)
+ self.assertEquals(None, control_message.cookie_path)
+
+ def test_password_auth(self):
+ """
+ Checks a response with password authentication.
+ """
+
+ control_message = mocking.get_message(PASSWORD_AUTH)
+ stem.response.convert("PROTOCOLINFO", control_message)
+ self.assertEquals((AuthMethod.PASSWORD, ), control_message.auth_methods)
+
+ def test_cookie_auth(self):
+ """
+ Checks a response with cookie authentication and a path including escape
+ characters.
+ """
+
+ control_message = mocking.get_message(COOKIE_AUTH)
+ stem.response.convert("PROTOCOLINFO", control_message)
+ self.assertEquals((AuthMethod.COOKIE, ), control_message.auth_methods)
+ self.assertEquals("/tmp/my data\\\"dir//control_auth_cookie", control_message.cookie_path)
+
+ def test_multiple_auth(self):
+ """
+ Checks a response with multiple authentication methods.
+ """
+
+ control_message = mocking.get_message(MULTIPLE_AUTH)
+ stem.response.convert("PROTOCOLINFO", control_message)
+ self.assertEquals((AuthMethod.COOKIE, AuthMethod.PASSWORD), control_message.auth_methods)
+ self.assertEquals("/home/atagar/.tor/control_auth_cookie", control_message.cookie_path)
+
+ def test_unknown_auth(self):
+ """
+ Checks a response with an unrecognized authtentication method.
+ """
+
+ control_message = mocking.get_message(UNKNOWN_AUTH)
+ stem.response.convert("PROTOCOLINFO", control_message)
+ self.assertEquals((AuthMethod.UNKNOWN, AuthMethod.PASSWORD), control_message.auth_methods)
+ self.assertEquals(("MAGIC", "PIXIE_DUST"), control_message.unknown_auth_methods)
+
+ def test_minimum_response(self):
+ """
+ Checks a PROTOCOLINFO response that only contains the minimum amount of
+ information to be a valid response.
+ """
+
+ control_message = mocking.get_message(MINIMUM_RESPONSE)
+ stem.response.convert("PROTOCOLINFO", control_message)
+
+ self.assertEquals(5, control_message.protocol_version)
+ self.assertEquals(None , control_message.tor_version)
+ self.assertEquals((), control_message.auth_methods)
+ self.assertEquals((), control_message.unknown_auth_methods)
+ self.assertEquals(None, control_message.cookie_path)
+
+ def test_relative_cookie(self):
+ """
+ Checks an authentication cookie with a relative path where expansion both
+ succeeds and fails.
+ """
+
+ # we need to mock both pid and cwd lookups since the general cookie
+ # expanion works by...
+ # - resolving the pid of the "tor" process
+ # - using that to get tor's cwd
+
+ def call_mocking(command):
+ if command == stem.util.system.GET_PID_BY_NAME_PGREP % "tor":
+ return ["10"]
+ elif command == stem.util.system.GET_CWD_PWDX % 10:
+ return ["10: /tmp/foo"]
+
+ mocking.mock(stem.util.proc.is_available, mocking.return_false())
+ mocking.mock(stem.util.system.is_available, mocking.return_true())
+ mocking.mock(stem.util.system.call, call_mocking)
+
+ control_message = mocking.get_message(RELATIVE_COOKIE_PATH)
+ stem.response.convert("PROTOCOLINFO", control_message)
+ self.assertEquals("/tmp/foo/tor-browser_en-US/Data/control_auth_cookie", control_message.cookie_path)
+
+ # exercise cookie expansion where both calls fail (should work, just
+ # leaving the path unexpanded)
+
+ mocking.mock(stem.util.system.call, mocking.return_none())
+ control_message = mocking.get_message(RELATIVE_COOKIE_PATH)
+ stem.response.convert("PROTOCOLINFO", control_message)
+ self.assertEquals("./tor-browser_en-US/Data/control_auth_cookie", control_message.cookie_path)
+
+ # reset system call mocking
+ mocking.revert_mocking()
+
1
0
28 May '12
commit 6015799baf9c0e6d3a618c948c32c27b2d7e1fc3
Author: Damian Johnson <atagar(a)torproject.org>
Date: Mon May 28 15:48:32 2012 -0700
Being more anal about 'OK' status checking
Refusing to parse messages that lack a 250 response on all lines, and checking
for a 'OK' status on the last line (and only the last line).
---
stem/response/getinfo.py | 10 +++++++---
stem/response/protocolinfo.py | 11 ++++++-----
2 files changed, 13 insertions(+), 8 deletions(-)
diff --git a/stem/response/getinfo.py b/stem/response/getinfo.py
index a13a18f..7cde9f1 100644
--- a/stem/response/getinfo.py
+++ b/stem/response/getinfo.py
@@ -23,9 +23,13 @@ class GetInfoResponse(stem.socket.ControlMessage):
self.values = {}
- for line in self:
- if line == "OK": break
- elif not "=" in line:
+ lines = list(self)
+
+ if not self.is_ok() or not lines.pop() == "OK":
+ raise stem.socket.ProtocolError("GETINFO response didn't have an OK status:\n%s" % self)
+
+ for line in lines:
+ if not "=" in line:
raise stem.socket.ProtocolError("GETINFO replies should only contain parameter=value mappings: %s" % line)
key, value = line.split("=", 1)
diff --git a/stem/response/protocolinfo.py b/stem/response/protocolinfo.py
index 4547a1d..97ecfb7 100644
--- a/stem/response/protocolinfo.py
+++ b/stem/response/protocolinfo.py
@@ -37,16 +37,17 @@ class ProtocolInfoResponse(stem.socket.ControlMessage):
self.cookie_path = None
auth_methods, unknown_auth_methods = [], []
+ lines = list(self)
+
+ if not self.is_ok() or not lines.pop() == "OK":
+ raise stem.socket.ProtocolError("GETINFO response didn't have an OK status:\n%s" % self)
# sanity check that we're a PROTOCOLINFO response
- if not list(self)[0].startswith("PROTOCOLINFO"):
+ if not lines[0].startswith("PROTOCOLINFO"):
msg = "Message is not a PROTOCOLINFO response (%s)" % self
raise stem.socket.ProtocolError(msg)
- for line in self:
- if line == "OK": break
- elif line.is_empty(): continue # blank line
-
+ for line in lines:
line_type = line.pop()
if line_type == "PROTOCOLINFO":
1
0
28 May '12
commit be06640765510b42912f074ebad6acb7cf4d0bac
Author: Damian Johnson <atagar(a)torproject.org>
Date: Mon May 28 15:52:18 2012 -0700
Renaming getinfo 'values' attribute to 'entries'
The name 'values' has bugged me for a while because it's a dictionary, so users
might end up typing things like "my_getinfo_response.values.values()". Renaming
it to something that's still generic but without these issues.
---
stem/control.py | 6 +++---
stem/response/getinfo.py | 10 +++++-----
test/unit/response/getinfo.py | 8 ++++----
3 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/stem/control.py b/stem/control.py
index b188ea4..1f3b767 100644
--- a/stem/control.py
+++ b/stem/control.py
@@ -480,7 +480,7 @@ class Controller(BaseController):
# error if we got back different parameters than we requested
requested_params = set(param)
- reply_params = set(response.values.keys())
+ reply_params = set(response.entries.keys())
if requested_params != reply_params:
requested_label = ", ".join(requested_params)
@@ -489,9 +489,9 @@ class Controller(BaseController):
raise stem.socket.ProtocolError("GETINFO reply doesn't match the parameters that we requested. Queried '%s' but got '%s'." % (requested_label, reply_label))
if is_multiple:
- return response.values
+ return response.entries
else:
- return response.values[param[0]]
+ return response.entries[param[0]]
except stem.socket.ControllerError, exc:
if default == UNDEFINED: raise exc
else: return default
diff --git a/stem/response/getinfo.py b/stem/response/getinfo.py
index 7cde9f1..dabbe12 100644
--- a/stem/response/getinfo.py
+++ b/stem/response/getinfo.py
@@ -5,7 +5,7 @@ class GetInfoResponse(stem.socket.ControlMessage):
Reply for a GETINFO query.
Attributes:
- values (dict) - mapping between the queried options and their values
+ entries (dict) - mapping between the queried options and their values
"""
def _parse_message(self):
@@ -21,7 +21,7 @@ class GetInfoResponse(stem.socket.ControlMessage):
# .
# 250 OK
- self.values = {}
+ self.entries = {}
lines = list(self)
@@ -30,7 +30,7 @@ class GetInfoResponse(stem.socket.ControlMessage):
for line in lines:
if not "=" in line:
- raise stem.socket.ProtocolError("GETINFO replies should only contain parameter=value mappings: %s" % line)
+ raise stem.socket.ProtocolError("GETINFO replies should only contain parameter=value mappings:\n%s" % self)
key, value = line.split("=", 1)
@@ -41,7 +41,7 @@ class GetInfoResponse(stem.socket.ControlMessage):
if value.startswith("\n"):
value = value[1:]
else:
- raise stem.socket.ProtocolError("GETINFO response contained a multiline value that didn't start with a newline: %s" % line)
+ raise stem.socket.ProtocolError("GETINFO response contained a multiline value that didn't start with a newline:\n%s" % self)
- self.values[key] = value
+ self.entries[key] = value
diff --git a/test/unit/response/getinfo.py b/test/unit/response/getinfo.py
index 5f5862a..6787cff 100644
--- a/test/unit/response/getinfo.py
+++ b/test/unit/response/getinfo.py
@@ -56,7 +56,7 @@ class TestGetInfoResponse(unittest.TestCase):
self.assertTrue(isinstance(control_message, stem.socket.ControlMessage))
self.assertTrue(isinstance(control_message, stem.response.getinfo.GetInfoResponse))
- self.assertEqual({}, control_message.values)
+ self.assertEqual({}, control_message.entries)
def test_single_response(self):
"""
@@ -65,7 +65,7 @@ class TestGetInfoResponse(unittest.TestCase):
control_message = mocking.get_message(SINGLE_RESPONSE)
stem.response.convert("GETINFO", control_message)
- self.assertEqual({"version": "0.2.3.11-alpha-dev"}, control_message.values)
+ self.assertEqual({"version": "0.2.3.11-alpha-dev"}, control_message.entries)
def test_batch_response(self):
"""
@@ -81,7 +81,7 @@ class TestGetInfoResponse(unittest.TestCase):
"fingerprint": "5FDE0422045DF0E1879A3738D09099EB4A0C5BA0",
}
- self.assertEqual(expected, control_message.values)
+ self.assertEqual(expected, control_message.entries)
def test_multiline_response(self):
"""
@@ -97,7 +97,7 @@ class TestGetInfoResponse(unittest.TestCase):
"config-text": "\n".join(MULTILINE_RESPONSE.splitlines()[2:8]),
}
- self.assertEqual(expected, control_message.values)
+ self.assertEqual(expected, control_message.entries)
def test_invalid_non_mapping_content(self):
"""
1
0