001: /****************************************************************
002: * Licensed to the Apache Software Foundation (ASF) under one *
003: * or more contributor license agreements. See the NOTICE file *
004: * distributed with this work for additional information *
005: * regarding copyright ownership. The ASF licenses this file *
006: * to you under the Apache License, Version 2.0 (the *
007: * "License"); you may not use this file except in compliance *
008: * with the License. You may obtain a copy of the License at *
009: * *
010: * http://www.apache.org/licenses/LICENSE-2.0 *
011: * *
012: * Unless required by applicable law or agreed to in writing, *
013: * software distributed under the License is distributed on an *
014: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
015: * KIND, either express or implied. See the License for the *
016: * specific language governing permissions and limitations *
017: * under the License. *
018: ****************************************************************/package org.apache.james.jspf.core;
019:
020: /**
021: * This Class is used to convert all macros which can used in SPF-Records to the
022: * right values!
023: *
024: */
025:
026: import org.apache.james.jspf.core.exceptions.NeutralException;
027: import org.apache.james.jspf.core.exceptions.NoneException;
028: import org.apache.james.jspf.core.exceptions.PermErrorException;
029: import org.apache.james.jspf.core.exceptions.TempErrorException;
030: import org.apache.james.jspf.core.exceptions.TimeoutException;
031:
032: import java.io.UnsupportedEncodingException;
033: import java.net.URLEncoder;
034: import java.util.ArrayList;
035: import java.util.Iterator;
036: import java.util.List;
037: import java.util.regex.Matcher;
038: import java.util.regex.Pattern;
039:
040: public class MacroExpand {
041:
042: private Pattern domainSpecPattern;
043:
044: private Pattern macroStringPattern;
045:
046: private Pattern macroLettersPattern;
047:
048: private Pattern macroLettersExpPattern;
049:
050: private Pattern cellPattern;
051:
052: private Logger log;
053:
054: private DNSService dnsProbe;
055:
056: public static final boolean EXPLANATION = true;
057:
058: public static final boolean DOMAIN = false;
059:
060: public static class RequireClientDomainException extends Exception {
061:
062: private static final long serialVersionUID = 3834282981657676530L;
063:
064: }
065:
066: /**
067: * Construct MacroExpand
068: *
069: * @param logger the logget to use
070: * @param dnsProbe the dns service to use
071: */
072: public MacroExpand(Logger logger, DNSService dnsProbe) {
073: // This matches 2 groups
074: domainSpecPattern = Pattern
075: .compile(SPFTermsRegexps.DOMAIN_SPEC_REGEX_R);
076: // The real pattern replacer
077: macroStringPattern = Pattern
078: .compile(SPFTermsRegexps.MACRO_STRING_REGEX_TOKEN);
079: // The macro letters pattern
080: macroLettersExpPattern = Pattern
081: .compile(SPFTermsRegexps.MACRO_LETTER_PATTERN_EXP);
082: macroLettersPattern = Pattern
083: .compile(SPFTermsRegexps.MACRO_LETTER_PATTERN);
084: log = logger;
085: this .dnsProbe = dnsProbe;
086: }
087:
088: private static final class AResponseListener implements
089: SPFCheckerDNSResponseListener {
090:
091: /**
092: * @see org.apache.james.jspf.core.SPFCheckerDNSResponseListener#onDNSResponse(org.apache.james.jspf.core.DNSResponse, org.apache.james.jspf.core.SPFSession)
093: */
094: public DNSLookupContinuation onDNSResponse(
095: DNSResponse response, SPFSession session)
096: throws PermErrorException, NoneException,
097: TempErrorException, NeutralException {
098: // just return the default "unknown" if we cannot find anything
099: // later
100: session.setClientDomain("unknown");
101: try {
102: List records = response.getResponse();
103: if (records != null && records.size() > 0) {
104: Iterator i = records.iterator();
105: while (i.hasNext()) {
106: String next = (String) i.next();
107: if (IPAddr.getAddress(session.getIpAddress())
108: .toString().equals(
109: IPAddr.getAddress(next)
110: .toString())) {
111: session
112: .setClientDomain((String) session
113: .getAttribute(ATTRIBUTE_MACRO_EXPAND_CHECKED_RECORD));
114: break;
115: }
116: }
117: }
118: } catch (TimeoutException e) {
119: // just return the default "unknown".
120: } catch (PermErrorException e) {
121: // just return the default "unknown".
122: }
123: return null;
124: }
125: }
126:
127: private static final class PTRResponseListener implements
128: SPFCheckerDNSResponseListener {
129:
130: /**
131: * @see org.apache.james.jspf.core.SPFCheckerDNSResponseListener#onDNSResponse(org.apache.james.jspf.core.DNSResponse, org.apache.james.jspf.core.SPFSession)
132: */
133: public DNSLookupContinuation onDNSResponse(
134: DNSResponse response, SPFSession session)
135: throws PermErrorException, NoneException,
136: TempErrorException, NeutralException {
137:
138: try {
139: boolean ip6 = IPAddr.isIPV6(session.getIpAddress());
140: List records = response.getResponse();
141:
142: if (records != null && records.size() > 0) {
143: String record = (String) records.get(0);
144: session.setAttribute(
145: ATTRIBUTE_MACRO_EXPAND_CHECKED_RECORD,
146: record);
147:
148: return new DNSLookupContinuation(new DNSRequest(
149: record, ip6 ? DNSRequest.AAAA
150: : DNSRequest.A),
151: new AResponseListener());
152:
153: }
154: } catch (TimeoutException e) {
155: // just return the default "unknown".
156: session.setClientDomain("unknown");
157: }
158: return null;
159:
160: }
161: }
162:
163: private static final String ATTRIBUTE_MACRO_EXPAND_CHECKED_RECORD = "MacroExpand.checkedRecord";
164:
165: public DNSLookupContinuation checkExpand(String input,
166: SPFSession session, boolean isExplanation)
167: throws PermErrorException, NoneException {
168: if (input != null) {
169: String host = this .expand(input, session, isExplanation);
170: if (host == null) {
171:
172: return new DNSLookupContinuation(new DNSRequest(IPAddr
173: .getAddress(session.getIpAddress())
174: .getReverseIP(), DNSRequest.PTR),
175: new PTRResponseListener());
176: }
177: }
178: return null;
179: }
180:
181: public String expand(String input, MacroData macroData,
182: boolean isExplanation) throws PermErrorException {
183: try {
184: if (isExplanation) {
185: return expandExplanation(input, macroData);
186: } else {
187: return expandDomain(input, macroData);
188: }
189: } catch (RequireClientDomainException e) {
190: return null;
191: }
192: }
193:
194: /**
195: * This method expand the given a explanation
196: *
197: * @param input
198: * The explanation which should be expanded
199: * @return expanded The expanded explanation
200: * @throws PermErrorException
201: * Get thrown if invalid macros are used
202: * @throws RequireClientDomain
203: */
204: private String expandExplanation(String input, MacroData macroData)
205: throws PermErrorException, RequireClientDomainException {
206:
207: log.debug("Start do expand explanation: " + input);
208:
209: String[] parts = input.split(" ");
210: StringBuffer res = new StringBuffer();
211: for (int i = 0; i < parts.length; i++) {
212: if (i > 0)
213: res.append(" ");
214: res.append(expandMacroString(parts[i], macroData, true));
215: }
216: log.debug("Done expand explanation: " + res);
217:
218: return res.toString();
219: }
220:
221: /**
222: * This method expand the given domain. So all known macros get replaced
223: *
224: * @param input
225: * The domain which should be expand
226: * @return expanded The domain with replaced macros
227: * @throws PermErrorException
228: * This get thrown if invalid macros are used
229: * @throws RequireClientDomain
230: */
231: private String expandDomain(String input, MacroData macroData)
232: throws PermErrorException, RequireClientDomainException {
233:
234: log.debug("Start expand domain: " + input);
235:
236: Matcher inputMatcher = domainSpecPattern.matcher(input);
237: if (!inputMatcher.matches() || inputMatcher.groupCount() != 2) {
238: throw new PermErrorException("Invalid DomainSpec: " + input);
239: }
240:
241: StringBuffer res = new StringBuffer();
242: if (inputMatcher.group(1) != null
243: && inputMatcher.group(1).length() > 0) {
244: res.append(expandMacroString(inputMatcher.group(1),
245: macroData, false));
246: }
247: if (inputMatcher.group(2) != null
248: && inputMatcher.group(2).length() > 0) {
249: if (inputMatcher.group(2).startsWith(".")) {
250: res.append(inputMatcher.group(2));
251: } else {
252: res.append(expandMacroString(inputMatcher.group(2),
253: macroData, false));
254: }
255: }
256:
257: String domainName = expandMacroString(input, macroData, false);
258: // reduce to less than 255 characters, deleting subdomains from left
259: int split = 0;
260: while (domainName.length() > 255 && split > -1) {
261: split = domainName.indexOf(".");
262: domainName = domainName.substring(split + 1);
263: }
264:
265: log.debug("Domain expanded: " + domainName);
266:
267: return domainName;
268: }
269:
270: /**
271: * Expand the given String
272: *
273: * @param input
274: * The inputString which should get expanded
275: * @return expanded The expanded given String
276: * @throws PermErrorException
277: * This get thrown if invalid macros are used
278: * @throws RequireClientDomain
279: */
280: private String expandMacroString(String input, MacroData macroData,
281: boolean isExplanation) throws PermErrorException,
282: RequireClientDomainException {
283:
284: StringBuffer decodedValue = new StringBuffer();
285: Matcher inputMatcher = macroStringPattern.matcher(input);
286: String macroCell;
287: int pos = 0;
288:
289: while (inputMatcher.find()) {
290: String match2 = inputMatcher.group();
291: if (pos != inputMatcher.start()) {
292: throw new PermErrorException(
293: "Middle part does not match: "
294: + input.substring(0, pos)
295: + ">>"
296: + input.substring(pos, inputMatcher
297: .start()) + "<<"
298: + input.substring(inputMatcher.start())
299: + " [" + input + "]");
300: }
301: if (match2.length() > 0) {
302: if (match2.startsWith("%{")) {
303: macroCell = input.substring(
304: inputMatcher.start() + 2, inputMatcher
305: .end() - 1);
306: inputMatcher.appendReplacement(decodedValue,
307: replaceCell(macroCell, macroData,
308: isExplanation));
309: } else if (match2.length() == 2
310: && match2.startsWith("%")) {
311: // handle the % escaping
312: inputMatcher.appendReplacement(decodedValue, match2
313: .substring(1));
314: }
315: }
316:
317: pos = inputMatcher.end();
318: }
319:
320: if (input.length() != pos) {
321: throw new PermErrorException("End part does not match: "
322: + input.substring(pos));
323: }
324:
325: inputMatcher.appendTail(decodedValue);
326:
327: return decodedValue.toString();
328: }
329:
330: /**
331: * Replace the macros in given String
332: *
333: * @param replaceValue
334: * The String in which known macros should get replaced
335: * @return returnData The String with replaced macros
336: * @throws PermErrorException
337: * Get thrown if an error in processing happen
338: * @throws RequireClientDomain
339: */
340: private String replaceCell(String replaceValue,
341: MacroData macroData, boolean isExplanation)
342: throws PermErrorException, RequireClientDomainException {
343:
344: String variable = "";
345: String domainNumber = "";
346: boolean isReversed = false;
347: String delimeters = ".";
348:
349: // Get only command character so that 'r' command and 'r' modifier don't
350: // clash
351: String commandCharacter = replaceValue.substring(0, 1);
352: Matcher cellMatcher;
353: // Find command
354: if (isExplanation) {
355: cellMatcher = macroLettersExpPattern
356: .matcher(commandCharacter);
357: } else {
358: cellMatcher = macroLettersPattern.matcher(commandCharacter);
359: }
360: if (cellMatcher.find()) {
361: if (cellMatcher.group().toUpperCase().equals(
362: cellMatcher.group())) {
363: variable = encodeURL(matchMacro(cellMatcher.group(),
364: macroData));
365: } else {
366: variable = matchMacro(cellMatcher.group(), macroData);
367: }
368: // Remove Macro code so that r macro code does not clash with r the
369: // reverse modifier
370: replaceValue = replaceValue.substring(1);
371: } else {
372: throw new PermErrorException("MacroLetter not found: "
373: + replaceValue);
374: }
375:
376: // Find number of domains to use
377: cellPattern = Pattern.compile("\\d+");
378: cellMatcher = cellPattern.matcher(replaceValue);
379: while (cellMatcher.find()) {
380: domainNumber = cellMatcher.group();
381: if (Integer.parseInt(domainNumber) == 0) {
382: throw new PermErrorException(
383: "Digit transformer must be non-zero");
384: }
385: }
386: // find if reversed
387: cellPattern = Pattern.compile("r");
388: cellMatcher = cellPattern.matcher(replaceValue);
389: while (cellMatcher.find()) {
390: isReversed = true;
391: }
392:
393: // find delimeters
394: cellPattern = Pattern.compile("[\\.\\-\\+\\,\\/\\_\\=]+");
395: cellMatcher = cellPattern.matcher(replaceValue);
396: while (cellMatcher.find()) {
397: delimeters = cellMatcher.group();
398: }
399:
400: // Reverse domains as necessary
401: ArrayList data = split(variable, delimeters);
402: if (isReversed) {
403: data = reverse(data);
404: }
405:
406: // Truncate domain name to number of sub sections
407: String returnData;
408: if (!domainNumber.equals("")) {
409: returnData = subset(data, Integer.parseInt(domainNumber));
410: } else {
411: returnData = subset(data);
412: }
413:
414: return returnData;
415:
416: }
417:
418: /**
419: * Get the value for the given macro like descripted in the RFC
420: *
421: * @param macro
422: * The macro we want to get the value for
423: * @return rValue The value for the given macro
424: * @throws PermErrorException
425: * Get thrown if the given variable is an unknown macro
426: * @throws RequireClientDomain requireClientDomain if the client domain is needed
427: * and not yet resolved.
428: */
429: private String matchMacro(String macro, MacroData macroData)
430: throws PermErrorException, RequireClientDomainException {
431:
432: String rValue = null;
433:
434: String variable = macro.toLowerCase();
435: if (variable.equalsIgnoreCase("i")) {
436: rValue = macroData.getMacroIpAddress();
437: } else if (variable.equalsIgnoreCase("s")) {
438: rValue = macroData.getMailFrom();
439: } else if (variable.equalsIgnoreCase("h")) {
440: rValue = macroData.getHostName();
441: } else if (variable.equalsIgnoreCase("l")) {
442: rValue = macroData.getCurrentSenderPart();
443: } else if (variable.equalsIgnoreCase("d")) {
444: rValue = macroData.getCurrentDomain();
445: } else if (variable.equalsIgnoreCase("v")) {
446: rValue = macroData.getInAddress();
447: } else if (variable.equalsIgnoreCase("t")) {
448: rValue = Long.toString(macroData.getTimeStamp());
449: } else if (variable.equalsIgnoreCase("c")) {
450: rValue = macroData.getReadableIP();
451: } else if (variable.equalsIgnoreCase("p")) {
452: rValue = macroData.getClientDomain();
453: if (rValue == null) {
454: throw new RequireClientDomainException();
455: }
456: } else if (variable.equalsIgnoreCase("o")) {
457: rValue = macroData.getSenderDomain();
458: } else if (variable.equalsIgnoreCase("r")) {
459: rValue = macroData.getReceivingDomain();
460: if (rValue == null) {
461: rValue = "unknown";
462: List dNames = dnsProbe.getLocalDomainNames();
463:
464: for (int i = 0; i < dNames.size(); i++) {
465: // check if the domainname is a FQDN
466: if (SPF1Utils.checkFQDN(dNames.get(i).toString())) {
467: rValue = dNames.get(i).toString();
468: if (macroData instanceof SPFSession) {
469: ((SPFSession) macroData)
470: .setReceivingDomain(rValue);
471: }
472: break;
473: }
474: }
475: }
476: }
477:
478: if (rValue == null) {
479: throw new PermErrorException("Unknown command : "
480: + variable);
481:
482: } else {
483: log.debug("Used macro: " + macro + " replaced with: "
484: + rValue);
485:
486: return rValue;
487: }
488: }
489:
490: /**
491: * Create an ArrayList by the given String. The String get splitted by given
492: * delimeters and one entry in the Array will be made for each splited
493: * String
494: *
495: * @param data
496: * The String we want to put in the Array
497: * @param delimeters
498: * The delimeter we want to use to split the String
499: * @return ArrayList which contains the String parts
500: */
501: private ArrayList split(String data, String delimeters) {
502:
503: String currentChar;
504: StringBuffer element = new StringBuffer();
505: ArrayList splitParts = new ArrayList();
506:
507: for (int i = 0; i < data.length(); i++) {
508: currentChar = data.substring(i, i + 1);
509: if (delimeters.indexOf(currentChar) > -1) {
510: splitParts.add(element.toString());
511: element.setLength(0);
512: } else {
513: element.append(currentChar);
514: }
515: }
516: splitParts.add(element.toString());
517: return splitParts;
518: }
519:
520: /**
521: * Reverse an ArrayList
522: *
523: * @param data
524: * The ArrayList we want to get reversed
525: * @return reversed The reversed given ArrayList
526: */
527: private ArrayList reverse(ArrayList data) {
528:
529: ArrayList reversed = new ArrayList();
530: for (int i = 0; i < data.size(); i++) {
531: reversed.add(0, data.get(i));
532: }
533: return reversed;
534: }
535:
536: /**
537: * @see #subset(ArrayList, int)
538: */
539: private String subset(ArrayList data) {
540: return subset(data, data.size());
541: }
542:
543: /**
544: * Convert a ArrayList to a String which holds the entries seperated by dots
545: *
546: * @param data The ArrayList which should be converted
547: * @param length The ArrayLength
548: * @return A String which holds all entries seperated by dots
549: */
550: private String subset(ArrayList data, int length) {
551:
552: StringBuffer buildString = new StringBuffer();
553: if (data.size() < length) {
554: length = data.size();
555: }
556: int start = data.size() - length;
557: for (int i = start; i < data.size(); i++) {
558: if (buildString.length() > 0) {
559: buildString.append(".");
560: }
561: buildString.append(data.get(i));
562: }
563: return buildString.toString();
564:
565: }
566:
567: /**
568: * Encode the given URL to UTF-8
569: *
570: * @param data
571: * url to encode
572: * @return encoded URL
573: */
574: private String encodeURL(String data) {
575:
576: try {
577: // TODO URLEncoder method is not RFC2396 compatible, known
578: // difference
579: // is Space character gets converted to "+" rather than "%20"
580: // Is there anything else which is not correct with URLEncoder?
581: // Couldn't find a RFC2396 encoder
582: data = URLEncoder.encode(data, "UTF-8");
583: } catch (UnsupportedEncodingException e) {
584: // This shouldn't happen ignore it!
585: }
586:
587: // workaround for the above descripted problem
588: return data.replaceAll("\\+", "%20");
589:
590: }
591:
592: }
|