note description: "[ The class provides an easy way to build HTTP header. You will also find some helper feature to help coding most common usage Please, have a look at constants classes such as HTTP_MIME_TYPES HTTP_HEADER_NAMES HTTP_STATUS_CODE HTTP_REQUEST_METHODS (or HTTP_CONSTANTS which groups them for convenience) Note the return status code is not part of the HTTP header ]" legal: "See notice at end of class." status: "See notice at end of class." date: "$Date$" revision: "$Revision$" class HTTP_HEADER inherit ITERABLE [READABLE_STRING_8] create make, make_with_count, make_from_array, make_from_header, make_from_raw_header_data convert make_from_array ({ARRAY [TUPLE [key: READABLE_STRING_8; value: READABLE_STRING_8]]}), string: {READABLE_STRING_8, STRING_8} feature {NONE} -- Initialization make -- Initialize current do make_with_count (3) end make_with_count (n: INTEGER) -- Make with a capacity of `n' header entries do create {ARRAYED_LIST [READABLE_STRING_8]} headers.make (n) end make_from_array (a_headers: ARRAY [TUPLE [key: READABLE_STRING_8; value: READABLE_STRING_8]]) -- Create HEADER from array of pair (key,value) do if a_headers.is_empty then make_with_count (0) else make_with_count (a_headers.count) append_array (a_headers) end end make_from_header (a_header: HTTP_HEADER) -- Create Current from existing HEADER `a_header' do make_with_count (a_header.headers.count) append_header_object (a_header) end make_from_raw_header_data (h: READABLE_STRING_8) -- Create Current from raw header data local line : detachable STRING lines: LIST [READABLE_STRING_8] do lines := h.split ('%N') make_with_count (lines.count) across lines as c loop line := c.item if not line.is_empty then if line [line.count] = '%R' then line.remove_tail (1) end add_header (line) end end end feature -- Recycle recycle -- Recycle current object do headers.wipe_out end feature -- Access count: INTEGER -- Number of header items. do Result := headers.count end is_empty: BOOLEAN -- Is empty? do Result := count = 0 end headers: ARRAYED_LIST [READABLE_STRING_8] -- Header's lines string: STRING_8 -- String representation of the headers local l_headers: like headers do l_headers := headers if not l_headers.is_empty then create Result.make (l_headers.count * 32) across l_headers as c loop append_line_to (c.item, Result) end else create Result.make_empty end ensure result_has_ending_cr_lf: Result.count >= 2 implies Result.substring (Result.count - 1, Result.count).same_string ("%R%N") result_has_single_ending_cr_lf: Result.count >= 4 implies not Result.substring (Result.count - 3, Result.count).same_string ("%R%N%R%N") end to_name_value_iterable: ITERABLE [TUPLE [name: READABLE_STRING_8; value: READABLE_STRING_8]] local res: ARRAYED_LIST [TUPLE [READABLE_STRING_8, READABLE_STRING_8]] do create res.make (headers.count) across headers as c loop if attached header_name_value (c.item) as tu then res.extend (tu) end end Result := res end feature -- Access new_cursor: INDEXABLE_ITERATION_CURSOR [READABLE_STRING_8] -- Fresh cursor associated with current structure do Result := headers.new_cursor end feature -- Header: filling append_array (a_headers: ARRAY [TUPLE [key: READABLE_STRING_8; value: READABLE_STRING_8]]) -- Append array of key,value headers do headers.grow (headers.count + a_headers.count) across a_headers as c loop put_header_key_value (c.item.key, c.item.value) end end append_header_object (h: HTTP_HEADER) -- Append headers from `h' do headers.grow (headers.count + h.headers.count) across h.headers as c loop add_header (c.item.string) end end feature -- Header change: general add_header (h: READABLE_STRING_8) -- Add header `h' -- if it already exists, there will be multiple header with same name -- which can also be valid require h_not_empty: not h.is_empty do headers.force (h) end put_header (h: READABLE_STRING_8) -- Add header `h' or replace existing header of same header name require h_not_empty: not h.is_empty do force_header_by_name (header_name_colon (h), h) end add_header_key_value (k,v: READABLE_STRING_8) -- Add header `k:v', or replace existing header of same header name/key do add_header (k + colon_space + v) end put_header_key_value (k,v: READABLE_STRING_8) -- Add header `k:v', or replace existing header of same header name/key do put_header (k + colon_space + v) end put_header_key_methods (k: READABLE_STRING_8; a_methods: ITERABLE [READABLE_STRING_8]) -- Add header `k: a_methods', or replace existing header of same header methods/key local s: STRING_8 do create s.make_empty across a_methods as c loop if not s.is_empty then s.append_string (", ") end s.append (c.item) end if not s.is_empty then put_header_key_value (k, s) end end feature -- Content related header put_content_type (t: READABLE_STRING_8) do put_header_key_value ({HTTP_HEADER_NAMES}.header_content_type, t) end add_content_type (t: READABLE_STRING_8) -- same as `put_content_type', but allow multiple definition of "Content-Type" do add_header_key_value ({HTTP_HEADER_NAMES}.header_content_type, t) end put_content_type_with_parameters (t: READABLE_STRING_8; a_params: detachable ARRAY [TUPLE [name: READABLE_STRING_8; value: READABLE_STRING_8]]) local s: STRING_8 do if a_params /= Void and then not a_params.is_empty then create s.make_from_string (t) across a_params as p loop if attached p.item as nv then s.append_character (';') s.append_character (' ') s.append (nv.name) s.append_character ('=') s.append_character ('%"') s.append (nv.value) s.append_character ('%"') end end put_header_key_value ({HTTP_HEADER_NAMES}.header_content_type, s) else put_content_type (t) end end add_content_type_with_parameters (t: READABLE_STRING_8; a_params: detachable ARRAY [TUPLE [name: READABLE_STRING_8; value: READABLE_STRING_8]]) local s: STRING_8 do if a_params /= Void and then not a_params.is_empty then create s.make_from_string (t) across a_params as p loop if attached p.item as nv then s.append_character (';') s.append_character (' ') s.append (nv.name) s.append_character ('=') s.append_character ('%"') s.append (nv.value) s.append_character ('%"') end end add_header_key_value ({HTTP_HEADER_NAMES}.header_content_type, s) else add_content_type (t) end end put_content_type_with_charset (t: READABLE_STRING_8; c: READABLE_STRING_8) do put_content_type_with_parameters (t, <<["charset", c]>>) end add_content_type_with_charset (t: READABLE_STRING_8; c: READABLE_STRING_8) -- same as `put_content_type_with_charset', but allow multiple definition of "Content-Type" do add_content_type_with_parameters (t, <<["charset", c]>>) end put_content_type_with_name (t: READABLE_STRING_8; n: READABLE_STRING_8) do put_content_type_with_parameters (t, <<["name", n]>>) end add_content_type_with_name (t: READABLE_STRING_8; n: READABLE_STRING_8) -- same as `put_content_type_with_name', but allow multiple definition of "Content-Type" do add_content_type_with_parameters (t, <<["name", n]>>) end put_content_length (n: INTEGER) do put_header_key_value ({HTTP_HEADER_NAMES}.header_content_length, n.out) end put_content_transfer_encoding (a_mechanism: READABLE_STRING_8) -- Put "Content-Transfer-Encoding" header with for instance "binary" --| encoding := "Content-Transfer-Encoding" ":" mechanism --| --| mechanism := "7bit" ; case-insensitive --| / "quoted-printable" --| / "base64" --| / "8bit" --| / "binary" --| / x-token do put_header_key_value ({HTTP_HEADER_NAMES}.header_content_transfer_encoding, a_mechanism) end put_transfer_encoding (a_enc: READABLE_STRING_8) -- Put "Transfer-Encoding" header with for instance "chunked" do put_header_key_value ({HTTP_HEADER_NAMES}.header_transfer_encoding, a_enc) end put_transfer_encoding_binary -- Put "Transfer-Encoding: binary" header do put_transfer_encoding (str_binary) end put_transfer_encoding_chunked -- Put "Transfer-Encoding: chunked" header do put_transfer_encoding (str_chunked) end put_content_disposition (a_type: READABLE_STRING_8; a_params: detachable READABLE_STRING_8) -- Put "Content-Disposition" header --| See RFC2183 --| disposition := "Content-Disposition" ":" --| disposition-type --| *(";" disposition-parm) --| disposition-type := "inline" --| / "attachment" --| / extension-token --| ; values are not case-sensitive --| disposition-parm := filename-parm --| / creation-date-parm --| / modification-date-parm --| / read-date-parm --| / size-parm --| / parameter --| filename-parm := "filename" "=" value --| creation-date-parm := "creation-date" "=" quoted-date-time --| modification-date-parm := "modification-date" "=" quoted-date-time --| read-date-parm := "read-date" "=" quoted-date-time --| size-parm := "size" "=" 1*DIGIT --| quoted-date-time := quoted-string --| ; contents MUST be an RFC 822 `date-time' --| ; numeric timezones (+HHMM or -HHMM) MUST be used do if a_params /= Void then put_header_key_value ({HTTP_HEADER_NAMES}.header_content_disposition, a_type + semi_colon_space + a_params) else put_header_key_value ({HTTP_HEADER_NAMES}.header_content_disposition, a_type) end end feature -- Content-type helpers put_content_type_text_css do put_content_type ({HTTP_MIME_TYPES}.text_css) end put_content_type_text_csv do put_content_type ({HTTP_MIME_TYPES}.text_csv) end put_content_type_text_html do put_content_type ({HTTP_MIME_TYPES}.text_html) end put_content_type_text_javascript do put_content_type ({HTTP_MIME_TYPES}.text_javascript) end put_content_type_text_json do put_content_type ({HTTP_MIME_TYPES}.text_json) end put_content_type_text_plain do put_content_type ({HTTP_MIME_TYPES}.text_plain) end put_content_type_text_xml do put_content_type ({HTTP_MIME_TYPES}.text_xml) end put_content_type_application_json do put_content_type ({HTTP_MIME_TYPES}.application_json) end put_content_type_application_javascript do put_content_type ({HTTP_MIME_TYPES}.application_javascript) end put_content_type_application_zip do put_content_type ({HTTP_MIME_TYPES}.application_zip) end put_content_type_application_pdf do put_content_type ({HTTP_MIME_TYPES}.application_pdf) end put_content_type_image_gif do put_content_type ({HTTP_MIME_TYPES}.image_gif) end put_content_type_image_png do put_content_type ({HTTP_MIME_TYPES}.image_png) end put_content_type_image_jpg do put_content_type ({HTTP_MIME_TYPES}.image_jpg) end put_content_type_image_svg_xml do put_content_type ({HTTP_MIME_TYPES}.image_svg_xml) end put_content_type_message_http do put_content_type ({HTTP_MIME_TYPES}.message_http) end put_content_type_multipart_mixed do put_content_type ({HTTP_MIME_TYPES}.multipart_mixed) end put_content_type_multipart_alternative do put_content_type ({HTTP_MIME_TYPES}.multipart_alternative) end put_content_type_multipart_related do put_content_type ({HTTP_MIME_TYPES}.multipart_related) end put_content_type_multipart_form_data do put_content_type ({HTTP_MIME_TYPES}.multipart_form_data) end put_content_type_multipart_signed do put_content_type ({HTTP_MIME_TYPES}.multipart_signed) end put_content_type_multipart_encrypted do put_content_type ({HTTP_MIME_TYPES}.multipart_encrypted) end put_content_type_application_x_www_form_encoded do put_content_type ({HTTP_MIME_TYPES}.application_x_www_form_encoded) end feature -- Cross-Origin Resource Sharing put_access_control_allow_origin (s: READABLE_STRING_8) -- Put "Access-Control-Allow-Origin" header. do put_header_key_value ({HTTP_HEADER_NAMES}.header_access_control_allow_origin, s) end put_access_control_allow_all_origin -- Put "Access-Control-Allow-Origin: *" header. do put_access_control_allow_origin ("*") end put_access_control_allow_methods (a_methods: ITERABLE [READABLE_STRING_8]) -- If `a_methods' is not empty, put `Access-Control-Allow-Methods' header with list `a_methods' of methods do put_header_key_methods ({HTTP_HEADER_NAMES}.header_access_control_allow_methods, a_methods) end put_access_control_allow_headers (s: READABLE_STRING_8) -- Put "Access-Control-Allow-Headers" header. do put_header_key_value ({HTTP_HEADER_NAMES}.header_access_control_allow_headers, s) end feature -- Method related put_allow (a_methods: ITERABLE [READABLE_STRING_8]) -- If `a_methods' is not empty, put `Allow' header with list `a_methods' of methods do put_header_key_methods ({HTTP_HEADER_NAMES}.header_allow, a_methods) end feature -- Date put_date (s: READABLE_STRING_8) -- Put "Date: " header do put_header_key_value ({HTTP_HEADER_NAMES}.header_date, s) end put_current_date -- Put current date time with "Date" header do put_utc_date (create {DATE_TIME}.make_now_utc) end put_utc_date (a_utc_date: DATE_TIME) -- Put UTC date time `dt' with "Date" header do put_date (date_to_rfc1123_http_date_format (a_utc_date)) end put_last_modified (a_utc_date: DATE_TIME) -- Put UTC date time `dt' with "Date" header do put_header_key_value ({HTTP_HEADER_NAMES}.header_last_modified, date_to_rfc1123_http_date_format (a_utc_date)) end feature -- Authorization put_authorization (s: READABLE_STRING_8) -- Put authorization `s' with "Authorization" header do put_header_key_value ({HTTP_HEADER_NAMES}.header_authorization, s) end feature -- Others put_expires (sec: INTEGER) do put_expires_string (sec.out) end put_expires_string (s: STRING) do put_header_key_value ("Expires", s) end put_expires_date (a_utc_date: DATE_TIME) do put_header_key_value ("Expires", date_to_rfc1123_http_date_format (a_utc_date)) end put_cache_control (s: READABLE_STRING_8) -- `s' could be for instance "no-cache, must-revalidate" do put_header_key_value ("Cache-Control", s) end put_pragma (s: READABLE_STRING_8) do put_header_key_value ("Pragma", s) end put_pragma_no_cache do put_pragma ("no-cache") end feature -- Redirection put_location (a_location: READABLE_STRING_8) -- Tell the client the new location `a_location' require a_location_valid: not a_location.is_empty do put_header_key_value ({HTTP_HEADER_NAMES}.header_location, a_location) end put_refresh (a_location: READABLE_STRING_8; a_timeout_in_seconds: INTEGER) -- Tell the client to refresh page with `a_location' after `a_timeout_in_seconds' in seconds require a_location_valid: not a_location.is_empty do put_header_key_value ({HTTP_HEADER_NAMES}.header_refresh, a_timeout_in_seconds.out + "; url=" + a_location) end feature -- Cookie put_cookie (key, value: READABLE_STRING_8; expiration, path, domain: detachable READABLE_STRING_8; secure, http_only: BOOLEAN) -- Set a cookie on the client's machine -- with key 'key' and value 'value'. -- Note: you should avoid using "localhost" as `domain' for local cookies -- since they are not always handled by browser (for instance Chrome) require make_sense: (key /= Void and value /= Void) and then (not key.is_empty and not value.is_empty) domain_without_port_info: domain /= Void implies domain.index_of (':', 1) = 0 local s: STRING do s := {HTTP_HEADER_NAMES}.header_set_cookie + colon_space + key + "=" + value if domain /= Void and then not domain.same_string ("localhost") then s.append ("; Domain=" + domain) end if path /= Void then s.append ("; Path=" + path) end if expiration /= Void then s.append ("; Expires=" + expiration) end if secure then s.append ("; Secure") end if http_only then s.append ("; HttpOnly") end add_header (s) end put_cookie_with_expiration_date (key, value: READABLE_STRING_8; expiration: DATE_TIME; path, domain: detachable READABLE_STRING_8; secure, http_only: BOOLEAN) -- Set a cookie on the client's machine -- with key 'key' and value 'value'. require make_sense: (key /= Void and value /= Void) and then (not key.is_empty and not value.is_empty) do put_cookie (key, value, date_to_rfc1123_http_date_format (expiration), path, domain, secure, http_only) end feature -- Status report header_named_value (a_name: READABLE_STRING_8): detachable STRING_8 -- Has header item for `n'? require has_header: has_header_named (a_name) local c: like headers.new_cursor n: INTEGER l_line: READABLE_STRING_8 do from n := a_name.count c := headers.new_cursor until c.after or Result /= Void loop l_line := c.item if l_line.starts_with (a_name) then if l_line.valid_index (n + 1) then if l_line [n + 1] = ':' then Result := l_line.substring (n + 2, l_line.count) Result.left_adjust Result.right_adjust end end end c.forth end end has_header_named (a_name: READABLE_STRING_8): BOOLEAN -- Has header item for `n'? local c: like headers.new_cursor n: INTEGER l_line: READABLE_STRING_8 do from n := a_name.count c := headers.new_cursor until c.after or Result loop l_line := c.item if l_line.starts_with (a_name) then if l_line.valid_index (n + 1) then Result := l_line [n + 1] = ':' end end c.forth end end has_content_length: BOOLEAN -- Has header "Content-Length" do Result := has_header_named ({HTTP_HEADER_NAMES}.header_content_length) end has_content_type: BOOLEAN -- Has header "Content-Type" do Result := has_header_named ({HTTP_HEADER_NAMES}.header_content_type) end has_transfer_encoding_chunked: BOOLEAN -- Has "Transfer-Encoding: chunked" header do if has_header_named ({HTTP_HEADER_NAMES}.header_transfer_encoding) then Result := attached header_named_value ({HTTP_HEADER_NAMES}.header_transfer_encoding) as v and then v.same_string (str_chunked) end end feature {NONE} -- Implementation: Header force_header_by_name (n: detachable READABLE_STRING_8; h: READABLE_STRING_8) -- Add header `h' or replace existing header of same header name `n' require h_has_name_n: (n /= Void and attached header_name_colon (h) as hn) implies n.same_string (hn) local l_headers: like headers do if n /= Void then from l_headers := headers l_headers.start until l_headers.after or l_headers.item.starts_with (n) loop l_headers.forth end if not l_headers.after then l_headers.replace (h) else add_header (h) end else add_header (h) end end header_name_colon (h: READABLE_STRING_8): detachable STRING_8 -- If any, header's name with colon --| ex: for "Foo-bar: something", this will return "Foo-bar:" local s: detachable STRING_8 i,n: INTEGER c: CHARACTER do from i := 1 n := h.count create s.make (10) until i > n or c = ':' or s = Void loop c := h[i] inspect c when ':' then s.extend (c) when '-', 'a' .. 'z', 'A' .. 'Z' then s.extend (c) else s := Void end i := i + 1 end Result := s end header_name_value (h: READABLE_STRING_8): detachable TUPLE [name: READABLE_STRING_8; value: READABLE_STRING_8] -- If any, header's [name,value] --| ex: for "Foo-bar: something", this will return ["Foo-bar", "something"] local s: detachable STRING_8 i,n: INTEGER c: CHARACTER do from i := 1 n := h.count create s.make (10) until i > n or c = ':' or s = Void loop c := h[i] inspect c when ':' then when '-', 'a' .. 'z', 'A' .. 'Z' then s.extend (c) else s := Void end i := i + 1 end if s /= Void then Result := [s, h.substring (i, n)] end end feature {NONE} -- Implementation append_line_to (s: READABLE_STRING_8; h: STRING_8) do h.append_string (s) append_end_of_line_to (h) end append_end_of_line_to (h: STRING_8) do h.append_character ('%R') h.append_character ('%N') end date_to_rfc1123_http_date_format (dt: DATE_TIME): STRING_8 -- String representation of `dt' using the RFC 1123 do Result := dt.formatted_out ("ddd, [0]dd mmm yyyy [0]hh:[0]mi:[0]ss") + " GMT" end feature {NONE} -- Constants str_binary: STRING = "binary" str_chunked: STRING = "chunked" colon_space: STRING = ": " semi_colon_space: STRING = "; " note copyright: "2011-2013, Jocelyn Fiat, Eiffel Software and others" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" source: "[ Eiffel Software 5949 Hollister Ave., Goleta, CA 93117 USA Telephone 805-685-1006, Fax 805-685-6869 Website http://www.eiffel.com Customer support http://support.eiffel.com ]" end