@@ -123,6 +123,63 @@ get_tls_version(SV, Service) ->
123123 {error , lists :flatten (M )}
124124 end .
125125
126+ verify_hsts (Str ) ->
127+ Props = lists :map (
128+ fun (P ) ->
129+ P1 = [string :trim (S ) || S <- string :split (P , " =" )],
130+ case P1 of
131+ [K ] ->
132+ {K , undefined };
133+ [K , V ] ->
134+ {K , V }
135+ end
136+ end , string :split (Str , " ;" , all )),
137+ {Valid , Invalid } = lists :partition (
138+ fun ({" includeSubDomains" , undefined }) ->
139+ true ;
140+ ({" preload" , undefined }) ->
141+ true ;
142+ ({" max-age" , Val }) ->
143+ Int = (catch erlang :list_to_integer (Val )),
144+ (is_integer (Int ) andalso (Int >= 0 ));
145+ (_ ) ->
146+ false
147+ end , Props ),
148+ case Invalid of
149+ [] ->
150+ case proplists :get_value (" max-age" , Valid ) of
151+ undefined ->
152+ {error , " max-age directive is required" };
153+ _ ->
154+ case length (lists :ukeysort (1 , Valid )) =:= length (Valid ) of
155+ false ->
156+ {error , " Cannot have duplicate directives" };
157+ true ->
158+ ok
159+ end
160+ end ;
161+ _ ->
162+ InvalidDir = [K || {K , _V } <- Invalid ],
163+ M = io_lib :format (" Invalid directives ~s " ,
164+ [lists :join (" ; " , InvalidDir )]),
165+ {error , lists :flatten (M )}
166+ end .
167+
168+ get_secure_headers (Json ) ->
169+ try ejson :decode (Json ) of
170+ {[{<<" Strict-Transport-Security" >>, BinStr }]} ->
171+ Str = binary_to_list (BinStr ),
172+ case verify_hsts (Str ) of
173+ ok -> {ok , [{" Strict-Transport-Security" , Str }]};
174+ Err -> Err
175+ end ;
176+ _ ->
177+ {error , " Only \" Strict-Transport-Security\" header allowed" }
178+ catch
179+ _ :_ ->
180+ {error , " Invalid format. Expecting a json." }
181+ end .
182+
126183get_cipher_suites (Str ) ->
127184 try ejson :decode (Str ) of
128185 L when is_list (L ) ->
@@ -177,10 +234,22 @@ services_with_security_settings() ->
177234 {cbas , analytics },
178235 {ns_server , clusterManager }].
179236
237+ is_allowed_on_cluster ([secure_headers ]) ->
238+ misc :is_strict_possible ();
239+ is_allowed_on_cluster (_ ) ->
240+ true .
241+
180242is_allowed_setting (K ) ->
181243 case cluster_compat_mode :is_enterprise () orelse not ee_only_settings (K ) of
182- true -> ok ;
183- false -> {error , <<" not supported in community edition" >>}
244+ true ->
245+ case is_allowed_on_cluster (K ) of
246+ true ->
247+ ok ;
248+ false ->
249+ {error , <<" Not supported in mixed version clusters." >>}
250+ end ;
251+ false ->
252+ {error , <<" not supported in community edition" >>}
184253 end .
185254
186255ee_only_settings ([ssl_minimum_protocol ]) -> true ;
@@ -193,6 +262,7 @@ conf(security) ->
193262 [{disable_ui_over_http , disableUIOverHttp , false , fun get_bool /1 },
194263 {disable_ui_over_https , disableUIOverHttps , false , fun get_bool /1 },
195264 {disable_www_authenticate , disableWWWAuthenticate , false , fun get_bool /1 },
265+ {secure_headers , responseHeaders , [], fun get_secure_headers /1 },
196266 {ui_session_timeout , uiSessionTimeout , undefined ,
197267 get_number (60 , 1000000 , undefined )},
198268 {ssl_minimum_protocol , tlsMinVersion ,
@@ -261,7 +331,18 @@ build_kvs(Conf, Config, Filter) ->
261331 undefined -> DV ;
262332 V -> V
263333 end ,
264- Filter ([CK ], Val ) andalso {true , {JK , Val }};
334+ case Filter ([CK ], Val ) of
335+ true ->
336+ case Val of
337+ [{_K , _V } | _ ] ->
338+ CVal = [{K , list_to_binary (V )} || {K , V } <- Val ],
339+ {true , {JK , {CVal }}};
340+ _ ->
341+ {true , {JK , Val }}
342+ end ;
343+ false ->
344+ false
345+ end ;
265346 ({CK , JK , SubKeys }) when is_list (SubKeys ) ->
266347 List = lists :filtermap (
267348 fun ({SubCK , SubJK , DV , _ }) ->
@@ -869,24 +950,31 @@ handle_settings_rebalance_post(Req) ->
869950
870951-ifdef (TEST ).
871952build_kvs_test () ->
872- Cfg = [[{key2 , value }, {key3 , [{sub_key2 , value }]}]],
953+ Cfg = [[{key2 , value },
954+ {key3 , [{sub_key2 , value }]},
955+ {key4 , [{key1 , " value1" }, {key2 , " value2" }]}]],
873956 Conf = [{key1 , jsonKey1 , default , fun (V ) -> {ok , V } end },
874957 {key2 , jsonKey2 , default , fun (V ) -> {ok , V } end },
875958 {key3 , jsonKey3 ,
876959 [{sub_key1 , subKey1 , default , fun (V ) -> {ok , V } end },
877- {sub_key2 , subKey2 , default , fun (V ) -> {ok , V } end }]}],
960+ {sub_key2 , subKey2 , default , fun (V ) -> {ok , V } end }]},
961+ {key4 , jsonKey4 , default , fun (V ) -> {ok , V } end }],
878962 ? assertEqual ([], build_kvs ([], [], fun (_ , _ ) -> true end )),
879963 ? assertEqual ([{jsonKey1 , default }, {jsonKey2 , value },
880- {jsonKey3 , {[{subKey1 , default }, {subKey2 , value }]}}],
964+ {jsonKey3 , {[{subKey1 , default }, {subKey2 , value }]}},
965+ {jsonKey4 , {[{key1 , <<" value1" >>}, {key2 , <<" value2" >>}]}}],
881966 build_kvs (Conf , Cfg , fun (_ , _ ) -> true end )),
882- ? assertEqual ([{jsonKey2 , value }, {jsonKey3 , {[{subKey2 , value }]}}],
967+ ? assertEqual ([{jsonKey2 , value },
968+ {jsonKey3 , {[{subKey2 , value }]}},
969+ {jsonKey4 , {[{key1 , <<" value1" >>}, {key2 , <<" value2" >>}]}}],
883970 build_kvs (Conf , Cfg ,
884971 fun (_ , default ) -> false ; (_ , _ ) -> true end )),
885972 ok .
886973
887974test_conf () ->
888975 [{ssl_minimum_protocol ,tlsMinVersion ,unused , get_tls_version (_ , all )},
889976 {cipher_suites ,cipherSuites ,unused , fun get_cipher_suites /1 },
977+ {secure_headers , responseHeaders , [], fun get_secure_headers /1 },
890978 {honor_cipher_order ,honorCipherOrder ,unused , fun get_bool /1 },
891979 {{security_settings , kv }, data ,
892980 [{cipher_suites , cipherSuites , unused , fun get_cipher_suites /1 },
@@ -898,19 +986,26 @@ test_conf() ->
898986
899987parse_post_data_test () ->
900988 Conf = test_conf (),
989+ RH = ejson :encode ({[{" Strict-Transport-Security" ,
990+ <<" max-age=10%3Bpreload%3BincludeSubDomains" >>}]}),
991+ ResponseHeaders = <<" responseHeaders=" , RH /binary , " &" >>,
901992 KeyValidator = fun ([not_allowed ]) -> {error , <<" not allowed" >>};
902993 (_ ) -> ok
903994 end ,
904995 ? assertEqual ({ok , []}, parse_post_data (Conf , [], <<>>, KeyValidator )),
905- ? assertEqual ({ok , [{[ssl_minimum_protocol ], 'tlsv1.2' },
996+ ? assertEqual ({ok , [{[secure_headers ],
997+ [{" Strict-Transport-Security" ,
998+ " max-age=10;preload;includeSubDomains" }]},
999+ {[ssl_minimum_protocol ], 'tlsv1.2' },
9061000 {[cipher_suites ], []},
9071001 {[honor_cipher_order ], true },
9081002 {[{security_settings , kv }, ssl_minimum_protocol ],
9091003 'tlsv1.3' },
9101004 {[{security_settings , kv }, cipher_suites ], []},
9111005 {[{security_settings , kv }, honor_cipher_order ], false }]},
9121006 parse_post_data (Conf , [],
913- <<" tlsMinVersion=tlsv1.2&"
1007+ <<ResponseHeaders /binary ,
1008+ " tlsMinVersion=tlsv1.2&"
9141009 " cipherSuites=[]&"
9151010 " honorCipherOrder=true&"
9161011 " data.tlsMinVersion=tlsv1.3&"
@@ -951,6 +1046,10 @@ parse_post_data_test() ->
9511046 " cipherSuites=[]&"
9521047 " cipherSuites=bad" >>,
9531048 KeyValidator )),
1049+ ? assertEqual ({error , [<<" responseHeaders - Invalid format. "
1050+ " Expecting a json." >>]},
1051+ parse_post_data (Conf , [" responseHeaders" ],
1052+ <<" bad" >>, KeyValidator )),
9541053 ? assertEqual ({error , [<<" data.cipherSuites - Invalid format. "
9551054 " Expecting a list of ciphers." >>]},
9561055 parse_post_data (Conf , [" data" , " cipherSuites" ],
0 commit comments