001package org.jsoup.safety;
002
003/*
004    Thank you to Ryan Grove (wonko.com) for the Ruby HTML cleaner http://github.com/rgrove/sanitize/, which inspired
005    this safe-list configuration, and the initial defaults.
006 */
007
008import org.jsoup.helper.Validate;
009import org.jsoup.internal.Normalizer;
010import org.jsoup.nodes.Attribute;
011import org.jsoup.nodes.Attributes;
012import org.jsoup.nodes.Element;
013
014import java.util.HashMap;
015import java.util.HashSet;
016import java.util.Iterator;
017import java.util.Map;
018import java.util.Objects;
019import java.util.Set;
020
021import static org.jsoup.internal.Normalizer.lowerCase;
022
023
024/**
025 Safe-lists define what HTML (elements and attributes) to allow through the cleaner. Everything else is removed.
026 <p>
027 Start with one of the defaults:
028 </p>
029 <ul>
030 <li>{@link #none}
031 <li>{@link #simpleText}
032 <li>{@link #basic}
033 <li>{@link #basicWithImages}
034 <li>{@link #relaxed}
035 </ul>
036 <p>
037 If you need to allow more through (please be careful!), tweak a base safelist with:
038 </p>
039 <ul>
040 <li>{@link #addTags(String... tagNames)}
041 <li>{@link #addAttributes(String tagName, String... attributes)}
042 <li>{@link #addEnforcedAttribute(String tagName, String attribute, String value)}
043 <li>{@link #addProtocols(String tagName, String attribute, String... protocols)}
044 </ul>
045 <p>
046 You can remove any setting from an existing safelist with:
047 </p>
048 <ul>
049 <li>{@link #removeTags(String... tagNames)}
050 <li>{@link #removeAttributes(String tagName, String... attributes)}
051 <li>{@link #removeEnforcedAttribute(String tagName, String attribute)}
052 <li>{@link #removeProtocols(String tagName, String attribute, String... removeProtocols)}
053 </ul>
054
055 <p>
056 The cleaner and these safelists assume that you want to clean a <code>body</code> fragment of HTML (to add user
057 supplied HTML into a templated page), and not to clean a full HTML document. If the latter is the case, you could wrap
058 the templated document HTML around the cleaned body HTML.
059 </p>
060 <p>
061 If you are going to extend a safelist, please be very careful. Make sure you understand what attributes may lead to
062 XSS attack vectors. URL attributes are particularly vulnerable and require careful validation. See 
063 the <a href="https://owasp.org/www-community/xss-filter-evasion-cheatsheet">XSS Filter Evasion Cheat Sheet</a> for some
064 XSS attack examples (that jsoup will safegaurd against the default Cleaner and Safelist configuration).
065 </p>
066 */
067public class Safelist {
068    private static final String All = ":all";
069    private final Set<TagName> tagNames; // tags allowed, lower case. e.g. [p, br, span]
070    private final Map<TagName, Set<AttributeKey>> attributes; // tag -> attribute[]. allowed attributes [href] for a tag.
071    private final Map<TagName, Map<AttributeKey, AttributeValue>> enforcedAttributes; // always set these attribute values
072    private final Map<TagName, Map<AttributeKey, Set<Protocol>>> protocols; // allowed URL protocols for attributes
073    private boolean preserveRelativeLinks; // option to preserve relative links
074
075    /**
076     This safelist allows only text nodes: any HTML Element or any Node other than a TextNode will be removed.
077     <p>
078     Note that the output of {@link org.jsoup.Jsoup#clean(String, Safelist)} is still <b>HTML</b> even when using
079     this Safelist, and so any HTML entities in the output will be appropriately escaped. If you want plain text, not
080     HTML, you should use a text method such as {@link Element#text()} instead, after cleaning the document.
081     </p>
082     <p>Example:</p>
083     <pre>{@code
084     String sourceBodyHtml = "<p>5 is &lt; 6.</p>";
085     String html = Jsoup.clean(sourceBodyHtml, Safelist.none());
086
087     Cleaner cleaner = new Cleaner(Safelist.none());
088     String text = cleaner.clean(Jsoup.parse(sourceBodyHtml)).text();
089
090     // html is: 5 is &lt; 6.
091     // text is: 5 is < 6.
092     }</pre>
093
094     @return safelist
095     */
096    public static Safelist none() {
097        return new Safelist();
098    }
099
100    /**
101     This safelist allows only simple text formatting: <code>b, em, i, strong, u</code>. All other HTML (tags and
102     attributes) will be removed.
103
104     @return safelist
105     */
106    public static Safelist simpleText() {
107        return new Safelist()
108                .addTags("b", "em", "i", "strong", "u")
109                ;
110    }
111
112    /**
113     <p>
114     This safelist allows a fuller range of text nodes: <code>a, b, blockquote, br, cite, code, dd, dl, dt, em, i, li,
115     ol, p, pre, q, small, span, strike, strong, sub, sup, u, ul</code>, and appropriate attributes.
116     </p>
117     <p>
118     Links (<code>a</code> elements) can point to <code>http, https, ftp, mailto</code>, and have an enforced
119     <code>rel=nofollow</code> attribute if they link offsite (as indicated by the specified base URI).
120     </p>
121     <p>
122     Does not allow images.
123     </p>
124
125     @return safelist
126     */
127    public static Safelist basic() {
128        return new Safelist()
129                .addTags(
130                        "a", "b", "blockquote", "br", "cite", "code", "dd", "dl", "dt", "em",
131                        "i", "li", "ol", "p", "pre", "q", "small", "span", "strike", "strong", "sub",
132                        "sup", "u", "ul")
133
134                .addAttributes("a", "href")
135                .addAttributes("blockquote", "cite")
136                .addAttributes("q", "cite")
137
138                .addProtocols("a", "href", "ftp", "http", "https", "mailto")
139                .addProtocols("blockquote", "cite", "http", "https")
140                .addProtocols("cite", "cite", "http", "https")
141
142                .addEnforcedAttribute("a", "rel", "nofollow") // has special handling for external links, in Cleaner
143                ;
144
145    }
146
147    /**
148     This safelist allows the same text tags as {@link #basic}, and also allows <code>img</code> tags, with appropriate
149     attributes, with <code>src</code> pointing to <code>http</code> or <code>https</code>.
150
151     @return safelist
152     */
153    public static Safelist basicWithImages() {
154        return basic()
155                .addTags("img")
156                .addAttributes("img", "align", "alt", "height", "src", "title", "width")
157                .addProtocols("img", "src", "http", "https")
158                ;
159    }
160
161    /**
162     This safelist allows a full range of text and structural body HTML: <code>a, b, blockquote, br, caption, cite,
163     code, col, colgroup, dd, div, dl, dt, em, h1, h2, h3, h4, h5, h6, i, img, li, ol, p, pre, q, small, span, strike, strong, sub,
164     sup, table, tbody, td, tfoot, th, thead, tr, u, ul</code>
165     <p>
166     Links do not have an enforced <code>rel=nofollow</code> attribute, but you can add that if desired.
167     </p>
168
169     @return safelist
170     */
171    public static Safelist relaxed() {
172        return new Safelist()
173                .addTags(
174                        "a", "b", "blockquote", "br", "caption", "cite", "code", "col",
175                        "colgroup", "dd", "div", "dl", "dt", "em", "h1", "h2", "h3", "h4", "h5", "h6",
176                        "i", "img", "li", "ol", "p", "pre", "q", "small", "span", "strike", "strong",
177                        "sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "u",
178                        "ul")
179
180                .addAttributes("a", "href", "title")
181                .addAttributes("blockquote", "cite")
182                .addAttributes("col", "span", "width")
183                .addAttributes("colgroup", "span", "width")
184                .addAttributes("img", "align", "alt", "height", "src", "title", "width")
185                .addAttributes("ol", "start", "type")
186                .addAttributes("q", "cite")
187                .addAttributes("table", "summary", "width")
188                .addAttributes("td", "abbr", "axis", "colspan", "rowspan", "width")
189                .addAttributes(
190                        "th", "abbr", "axis", "colspan", "rowspan", "scope",
191                        "width")
192                .addAttributes("ul", "type")
193
194                .addProtocols("a", "href", "ftp", "http", "https", "mailto")
195                .addProtocols("blockquote", "cite", "http", "https")
196                .addProtocols("cite", "cite", "http", "https")
197                .addProtocols("img", "src", "http", "https")
198                .addProtocols("q", "cite", "http", "https")
199                ;
200    }
201
202    /**
203     Create a new, empty safelist. Generally it will be better to start with a default prepared safelist instead.
204
205     @see #basic()
206     @see #basicWithImages()
207     @see #simpleText()
208     @see #relaxed()
209     */
210    public Safelist() {
211        tagNames = new HashSet<>();
212        attributes = new HashMap<>();
213        enforcedAttributes = new HashMap<>();
214        protocols = new HashMap<>();
215        preserveRelativeLinks = false;
216    }
217
218    /**
219     Deep copy an existing Safelist to a new Safelist.
220     @param copy the Safelist to copy
221     */
222    public Safelist(Safelist copy) {
223        this();
224        tagNames.addAll(copy.tagNames);
225        for (Map.Entry<TagName, Set<AttributeKey>> copyTagAttributes : copy.attributes.entrySet()) {
226            attributes.put(copyTagAttributes.getKey(), new HashSet<>(copyTagAttributes.getValue()));
227        }
228        for (Map.Entry<TagName, Map<AttributeKey, AttributeValue>> enforcedEntry : copy.enforcedAttributes.entrySet()) {
229            enforcedAttributes.put(enforcedEntry.getKey(), new HashMap<>(enforcedEntry.getValue()));
230        }
231        for (Map.Entry<TagName, Map<AttributeKey, Set<Protocol>>> protocolsEntry : copy.protocols.entrySet()) {
232            Map<AttributeKey, Set<Protocol>> attributeProtocolsCopy = new HashMap<>();
233            for (Map.Entry<AttributeKey, Set<Protocol>> attributeProtocols : protocolsEntry.getValue().entrySet()) {
234                attributeProtocolsCopy.put(attributeProtocols.getKey(), new HashSet<>(attributeProtocols.getValue()));
235            }
236            protocols.put(protocolsEntry.getKey(), attributeProtocolsCopy);
237        }
238        preserveRelativeLinks = copy.preserveRelativeLinks;
239    }
240
241    /**
242     Add a list of allowed elements to a safelist. (If a tag is not allowed, it will be removed from the HTML.)
243
244     @param tags tag names to allow
245     @return this (for chaining)
246     */
247    public Safelist addTags(String... tags) {
248        Validate.notNull(tags);
249
250        for (String tagName : tags) {
251            Validate.notEmpty(tagName);
252            Validate.isFalse(tagName.equalsIgnoreCase("noscript"),
253                "noscript is unsupported in Safelists, due to incompatibilities between parsers with and without script-mode enabled");
254            tagNames.add(TagName.valueOf(tagName));
255        }
256        return this;
257    }
258
259    /**
260     Remove a list of allowed elements from a safelist. (If a tag is not allowed, it will be removed from the HTML.)
261
262     @param tags tag names to disallow
263     @return this (for chaining)
264     */
265    public Safelist removeTags(String... tags) {
266        Validate.notNull(tags);
267
268        for(String tag: tags) {
269            Validate.notEmpty(tag);
270            TagName tagName = TagName.valueOf(tag);
271
272            if(tagNames.remove(tagName)) { // Only look in sub-maps if tag was allowed
273                attributes.remove(tagName);
274                enforcedAttributes.remove(tagName);
275                protocols.remove(tagName);
276            }
277        }
278        return this;
279    }
280
281    /**
282     Add a list of allowed attributes to a tag. (If an attribute is not allowed on an element, it will be removed.)
283     <p>
284     E.g.: <code>addAttributes("a", "href", "class")</code> allows <code>href</code> and <code>class</code> attributes
285     on <code>a</code> tags.
286     </p>
287     <p>
288     To make an attribute valid for <b>all tags</b>, use the pseudo tag <code>:all</code>, e.g.
289     <code>addAttributes(":all", "class")</code>.
290     </p>
291
292     @param tag  The tag the attributes are for. The tag will be added to the allowed tag list if necessary.
293     @param attributes List of valid attributes for the tag
294     @return this (for chaining)
295     */
296    public Safelist addAttributes(String tag, String... attributes) {
297        Validate.notEmpty(tag);
298        Validate.notNull(attributes);
299        Validate.isTrue(attributes.length > 0, "No attribute names supplied.");
300
301        addTags(tag);
302        TagName tagName = TagName.valueOf(tag);
303        Set<AttributeKey> attributeSet = new HashSet<>();
304        for (String key : attributes) {
305            Validate.notEmpty(key);
306            attributeSet.add(AttributeKey.valueOf(key));
307        }
308        Set<AttributeKey> currentSet = this.attributes.computeIfAbsent(tagName, k -> new HashSet<>());
309        currentSet.addAll(attributeSet);
310        return this;
311    }
312
313    /**
314     Remove a list of allowed attributes from a tag. (If an attribute is not allowed on an element, it will be removed.)
315     <p>
316     E.g.: <code>removeAttributes("a", "href", "class")</code> disallows <code>href</code> and <code>class</code>
317     attributes on <code>a</code> tags.
318     </p>
319     <p>
320     To make an attribute invalid for <b>all tags</b>, use the pseudo tag <code>:all</code>, e.g.
321     <code>removeAttributes(":all", "class")</code>.
322     </p>
323
324     @param tag  The tag the attributes are for.
325     @param attributes List of invalid attributes for the tag
326     @return this (for chaining)
327     */
328    public Safelist removeAttributes(String tag, String... attributes) {
329        Validate.notEmpty(tag);
330        Validate.notNull(attributes);
331        Validate.isTrue(attributes.length > 0, "No attribute names supplied.");
332
333        TagName tagName = TagName.valueOf(tag);
334        Set<AttributeKey> attributeSet = new HashSet<>();
335        for (String key : attributes) {
336            Validate.notEmpty(key);
337            attributeSet.add(AttributeKey.valueOf(key));
338        }
339        if(tagNames.contains(tagName) && this.attributes.containsKey(tagName)) { // Only look in sub-maps if tag was allowed
340            Set<AttributeKey> currentSet = this.attributes.get(tagName);
341            currentSet.removeAll(attributeSet);
342
343            if(currentSet.isEmpty()) // Remove tag from attribute map if no attributes are allowed for tag
344                this.attributes.remove(tagName);
345        }
346        if(tag.equals(All)) { // Attribute needs to be removed from all individually set tags
347            Iterator<Map.Entry<TagName, Set<AttributeKey>>> it = this.attributes.entrySet().iterator();
348            while (it.hasNext()) {
349                Map.Entry<TagName, Set<AttributeKey>> entry = it.next();
350                Set<AttributeKey> currentSet = entry.getValue();
351                currentSet.removeAll(attributeSet);
352                if(currentSet.isEmpty()) // Remove tag from attribute map if no attributes are allowed for tag
353                    it.remove();
354            }
355        }
356        return this;
357    }
358
359    /**
360     Add an enforced attribute to a tag. An enforced attribute will always be added to the element. If the element
361     already has the attribute set, it will be overridden with this value.
362     <p>
363     E.g.: <code>addEnforcedAttribute("a", "rel", "nofollow")</code> will make all <code>a</code> tags output as
364     <code>&lt;a href="..." rel="nofollow"&gt;</code>
365     </p>
366
367     @param tag   The tag the enforced attribute is for. The tag will be added to the allowed tag list if necessary.
368     @param attribute   The attribute name
369     @param value The enforced attribute value
370     @return this (for chaining)
371     */
372    public Safelist addEnforcedAttribute(String tag, String attribute, String value) {
373        Validate.notEmpty(tag);
374        Validate.notEmpty(attribute);
375        Validate.notEmpty(value);
376
377        TagName tagName = TagName.valueOf(tag);
378        tagNames.add(tagName);
379        AttributeKey attrKey = AttributeKey.valueOf(attribute);
380        AttributeValue attrVal = AttributeValue.valueOf(value);
381
382        Map<AttributeKey, AttributeValue> attrMap = enforcedAttributes.computeIfAbsent(tagName, k -> new HashMap<>());
383        attrMap.put(attrKey, attrVal);
384        return this;
385    }
386
387    /**
388     Remove a previously configured enforced attribute from a tag.
389
390     @param tag   The tag the enforced attribute is for.
391     @param attribute   The attribute name
392     @return this (for chaining)
393     */
394    public Safelist removeEnforcedAttribute(String tag, String attribute) {
395        Validate.notEmpty(tag);
396        Validate.notEmpty(attribute);
397
398        TagName tagName = TagName.valueOf(tag);
399        if(tagNames.contains(tagName) && enforcedAttributes.containsKey(tagName)) {
400            AttributeKey attrKey = AttributeKey.valueOf(attribute);
401            Map<AttributeKey, AttributeValue> attrMap = enforcedAttributes.get(tagName);
402            attrMap.remove(attrKey);
403
404            if(attrMap.isEmpty()) // Remove tag from enforced attribute map if no enforced attributes are present
405                enforcedAttributes.remove(tagName);
406        }
407        return this;
408    }
409
410    /**
411     * Configure this Safelist to preserve relative links in an element's URL attribute, or convert them to absolute
412     * links. By default, this is <b>false</b>: URLs will be  made absolute (e.g. start with an allowed protocol, like
413     * e.g. {@code http://}.
414     *
415     * @param preserve {@code true} to allow relative links, {@code false} (default) to deny
416     * @return this Safelist, for chaining.
417     * @see #addProtocols
418     */
419    public Safelist preserveRelativeLinks(boolean preserve) {
420        preserveRelativeLinks = preserve;
421        return this;
422    }
423
424    /**
425     * Get the current setting for preserving relative links.
426     * @return {@code true} if relative links are preserved, {@code false} if they are converted to absolute.
427     */
428    public boolean preserveRelativeLinks() {
429        return preserveRelativeLinks;
430    }
431
432    /**
433     Add allowed URL protocols for an element's URL attribute. This restricts the possible values of the attribute to
434     URLs with the defined protocol.
435     <p>
436     E.g.: <code>addProtocols("a", "href", "ftp", "http", "https")</code>
437     </p>
438     <p>
439     To allow a link to an in-page URL anchor (i.e. <code>&lt;a href="#anchor"&gt;</code>, add a <code>#</code>:<br>
440     E.g.: <code>addProtocols("a", "href", "#")</code>
441     </p>
442
443     @param tag       Tag the URL protocol is for
444     @param attribute       Attribute name
445     @param protocols List of valid protocols
446     @return this, for chaining
447     */
448    public Safelist addProtocols(String tag, String attribute, String... protocols) {
449        Validate.notEmpty(tag);
450        Validate.notEmpty(attribute);
451        Validate.notNull(protocols);
452
453        TagName tagName = TagName.valueOf(tag);
454        AttributeKey attrKey = AttributeKey.valueOf(attribute);
455        Map<AttributeKey, Set<Protocol>> attrMap = this.protocols.computeIfAbsent(tagName, k -> new HashMap<>());
456        Set<Protocol> protSet = attrMap.computeIfAbsent(attrKey, k -> new HashSet<>());
457
458        for (String protocol : protocols) {
459            Validate.notEmpty(protocol);
460            Protocol prot = Protocol.valueOf(protocol);
461            protSet.add(prot);
462        }
463        return this;
464    }
465
466    /**
467     Remove allowed URL protocols for an element's URL attribute. If you remove all protocols for an attribute, that
468     attribute will allow any protocol.
469     <p>
470     E.g.: <code>removeProtocols("a", "href", "ftp")</code>
471     </p>
472
473     @param tag Tag the URL protocol is for
474     @param attribute Attribute name
475     @param removeProtocols List of invalid protocols
476     @return this, for chaining
477     */
478    public Safelist removeProtocols(String tag, String attribute, String... removeProtocols) {
479        Validate.notEmpty(tag);
480        Validate.notEmpty(attribute);
481        Validate.notNull(removeProtocols);
482
483        TagName tagName = TagName.valueOf(tag);
484        AttributeKey attr = AttributeKey.valueOf(attribute);
485
486        // make sure that what we're removing actually exists; otherwise can open the tag to any data and that can
487        // be surprising
488        Validate.isTrue(protocols.containsKey(tagName), "Cannot remove a protocol that is not set.");
489        Map<AttributeKey, Set<Protocol>> tagProtocols = protocols.get(tagName);
490        Validate.isTrue(tagProtocols.containsKey(attr), "Cannot remove a protocol that is not set.");
491
492        Set<Protocol> attrProtocols = tagProtocols.get(attr);
493        for (String protocol : removeProtocols) {
494            Validate.notEmpty(protocol);
495            attrProtocols.remove(Protocol.valueOf(protocol));
496        }
497
498        if (attrProtocols.isEmpty()) { // Remove protocol set if empty
499            tagProtocols.remove(attr);
500            if (tagProtocols.isEmpty()) // Remove entry for tag if empty
501                protocols.remove(tagName);
502        }
503        return this;
504    }
505
506    /**
507     * Test if the supplied tag is allowed by this safelist.
508     * @param tag test tag
509     * @return true if allowed
510     */
511    public boolean isSafeTag(String tag) {
512        return tagNames.contains(TagName.valueOf(tag));
513    }
514
515    /**
516     * Test if the supplied attribute is allowed by this safelist for this tag.
517     * @param tagName tag to consider allowing the attribute in
518     * @param el element under test, to confirm protocol
519     * @param attr attribute under test
520     * @return true if allowed
521     */
522    public boolean isSafeAttribute(String tagName, Element el, Attribute attr) {
523        TagName tag = TagName.valueOf(tagName);
524        AttributeKey key = AttributeKey.valueOf(attr.getKey());
525
526        Set<AttributeKey> okSet = attributes.get(tag);
527        if (okSet != null && okSet.contains(key)) {
528            if (protocols.containsKey(tag)) {
529                Map<AttributeKey, Set<Protocol>> attrProts = protocols.get(tag);
530                // ok if not defined protocol; otherwise test
531                return !attrProts.containsKey(key) || testValidProtocol(el, attr, attrProts.get(key));
532            } else { // attribute found, no protocols defined, so OK
533                return true;
534            }
535        }
536        // might be an enforced attribute?
537        Map<AttributeKey, AttributeValue> enforcedSet = enforcedAttributes.get(tag);
538        if (enforcedSet != null) {
539            Attributes expect = getEnforcedAttributes(tagName);
540            String attrKey = attr.getKey();
541            if (expect.hasKeyIgnoreCase(attrKey)) {
542                return expect.getIgnoreCase(attrKey).equals(attr.getValue());
543            }
544        }
545        // no attributes defined for tag, try :all tag
546        return !tagName.equals(All) && isSafeAttribute(All, el, attr);
547    }
548
549    private boolean testValidProtocol(Element el, Attribute attr, Set<Protocol> protocols) {
550        // try to resolve relative urls to abs, and optionally update the attribute so output html has abs.
551        // rels without a baseuri get removed
552        String value = el.absUrl(attr.getKey());
553        if (value.length() == 0)
554            value = attr.getValue(); // if it could not be made abs, run as-is to allow custom unknown protocols
555        if (!preserveRelativeLinks)
556            attr.setValue(value);
557        
558        for (Protocol protocol : protocols) {
559            String prot = protocol.toString();
560
561            if (prot.equals("#")) { // allows anchor links
562                if (isValidAnchor(value)) {
563                    return true;
564                } else {
565                    continue;
566                }
567            }
568
569            prot += ":";
570
571            if (lowerCase(value).startsWith(prot)) {
572                return true;
573            }
574        }
575        return false;
576    }
577
578    private static boolean isValidAnchor(String value) {
579        return value.startsWith("#") && !value.matches(".*\\s.*");
580    }
581
582    /**
583     Gets the Attributes that should be enforced for a given tag
584     * @param tagName the tag
585     * @return the attributes that will be enforced; empty if none are set for the given tag
586     */
587    public Attributes getEnforcedAttributes(String tagName) {
588        Attributes attrs = new Attributes();
589        TagName tag = TagName.valueOf(tagName);
590        if (enforcedAttributes.containsKey(tag)) {
591            Map<AttributeKey, AttributeValue> keyVals = enforcedAttributes.get(tag);
592            for (Map.Entry<AttributeKey, AttributeValue> entry : keyVals.entrySet()) {
593                attrs.put(entry.getKey().toString(), entry.getValue().toString());
594            }
595        }
596        return attrs;
597    }
598    
599    // named types for config. All just hold strings, but here for my sanity.
600
601    static class TagName extends TypedValue {
602        TagName(String value) {
603            super(value);
604        }
605
606        static TagName valueOf(String value) {
607            return new TagName(Normalizer.lowerCase(value));
608        }
609    }
610
611    static class AttributeKey extends TypedValue {
612        AttributeKey(String value) {
613            super(value);
614        }
615
616        static AttributeKey valueOf(String value) {
617            return new AttributeKey(Normalizer.lowerCase(value));
618        }
619    }
620
621    static class AttributeValue extends TypedValue {
622        AttributeValue(String value) {
623            super(value);
624        }
625
626        static AttributeValue valueOf(String value) {
627            return new AttributeValue(value);
628        }
629    }
630
631    static class Protocol extends TypedValue {
632        Protocol(String value) {
633            super(value);
634        }
635
636        static Protocol valueOf(String value) {
637            return new Protocol(value);
638        }
639    }
640
641    abstract static class TypedValue {
642        private final String value;
643
644        TypedValue(String value) {
645            Validate.notNull(value);
646            this.value = value;
647        }
648
649        @Override
650        public int hashCode() {
651            return value.hashCode();
652        }
653
654        @Override
655        public boolean equals(Object obj) {
656            if (this == obj) return true;
657            if (obj == null || getClass() != obj.getClass()) return false;
658            TypedValue other = (TypedValue) obj;
659            return Objects.equals(value, other.value);
660        }
661
662        @Override
663        public String toString() {
664            return value;
665        }
666    }
667}