| margaretha | a048627 | 2018-04-12 19:59:31 +0200 | [diff] [blame] | 1 | package de.ids_mannheim.korap.web.controller; |
| 2 | |
| margaretha | 0512231 | 2018-04-16 15:01:34 +0200 | [diff] [blame] | 3 | import static org.junit.Assert.assertEquals; |
| margaretha | f839dde | 2018-04-16 17:52:57 +0200 | [diff] [blame] | 4 | import static org.junit.Assert.assertNotNull; |
| margaretha | fb027f9 | 2018-04-23 20:00:13 +0200 | [diff] [blame] | 5 | import static org.junit.Assert.assertTrue; |
| 6 | |
| 7 | import java.net.URI; |
| margaretha | 0512231 | 2018-04-16 15:01:34 +0200 | [diff] [blame] | 8 | |
| margaretha | a048627 | 2018-04-12 19:59:31 +0200 | [diff] [blame] | 9 | import javax.ws.rs.core.MultivaluedMap; |
| margaretha | 0512231 | 2018-04-16 15:01:34 +0200 | [diff] [blame] | 10 | import javax.ws.rs.core.Response.Status; |
| margaretha | a048627 | 2018-04-12 19:59:31 +0200 | [diff] [blame] | 11 | |
| 12 | import org.apache.http.entity.ContentType; |
| margaretha | 0512231 | 2018-04-16 15:01:34 +0200 | [diff] [blame] | 13 | import org.apache.oltu.oauth2.common.error.OAuthError; |
| margaretha | f839dde | 2018-04-16 17:52:57 +0200 | [diff] [blame] | 14 | import org.apache.oltu.oauth2.common.message.types.TokenType; |
| margaretha | a048627 | 2018-04-12 19:59:31 +0200 | [diff] [blame] | 15 | import org.junit.Test; |
| 16 | import org.springframework.beans.factory.annotation.Autowired; |
| 17 | |
| margaretha | 0512231 | 2018-04-16 15:01:34 +0200 | [diff] [blame] | 18 | import com.fasterxml.jackson.databind.JsonNode; |
| margaretha | a048627 | 2018-04-12 19:59:31 +0200 | [diff] [blame] | 19 | import com.google.common.net.HttpHeaders; |
| margaretha | a048627 | 2018-04-12 19:59:31 +0200 | [diff] [blame] | 20 | import com.sun.jersey.api.client.ClientResponse; |
| margaretha | a048627 | 2018-04-12 19:59:31 +0200 | [diff] [blame] | 21 | import com.sun.jersey.core.util.MultivaluedMapImpl; |
| 22 | |
| 23 | import de.ids_mannheim.korap.authentication.http.HttpAuthorizationHandler; |
| 24 | import de.ids_mannheim.korap.config.Attributes; |
| 25 | import de.ids_mannheim.korap.config.SpringJerseyTest; |
| 26 | import de.ids_mannheim.korap.exceptions.KustvaktException; |
| margaretha | 0512231 | 2018-04-16 15:01:34 +0200 | [diff] [blame] | 27 | import de.ids_mannheim.korap.utils.JsonUtils; |
| margaretha | a048627 | 2018-04-12 19:59:31 +0200 | [diff] [blame] | 28 | |
| 29 | /** |
| 30 | * @author margaretha |
| 31 | * |
| 32 | */ |
| 33 | public class OAuth2ControllerTest extends SpringJerseyTest { |
| 34 | |
| 35 | @Autowired |
| 36 | private HttpAuthorizationHandler handler; |
| margaretha | b4ce660 | 2018-04-26 20:23:57 +0200 | [diff] [blame^] | 37 | |
| margaretha | fb027f9 | 2018-04-23 20:00:13 +0200 | [diff] [blame] | 38 | private ClientResponse requestAuthorizationConfidentialClient ( |
| 39 | MultivaluedMap<String, String> form) throws KustvaktException { |
| 40 | |
| 41 | return resource().path("oauth2").path("authorize") |
| margaretha | f839dde | 2018-04-16 17:52:57 +0200 | [diff] [blame] | 42 | .header(Attributes.AUTHORIZATION, |
| 43 | handler.createBasicAuthorizationHeaderValue( |
| margaretha | fb027f9 | 2018-04-23 20:00:13 +0200 | [diff] [blame] | 44 | "fCBbQkAyYzI4NzUxMg", "secret")) |
| margaretha | f839dde | 2018-04-16 17:52:57 +0200 | [diff] [blame] | 45 | .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32") |
| 46 | .header(HttpHeaders.CONTENT_TYPE, |
| 47 | ContentType.APPLICATION_FORM_URLENCODED) |
| 48 | .entity(form).post(ClientResponse.class); |
| 49 | } |
| 50 | |
| margaretha | fb027f9 | 2018-04-23 20:00:13 +0200 | [diff] [blame] | 51 | @Test |
| 52 | public void testAuthorizeConfidentialClient () throws KustvaktException { |
| 53 | MultivaluedMap<String, String> form = new MultivaluedMapImpl(); |
| 54 | form.add("response_type", "code"); |
| 55 | form.add("client_id", "fCBbQkAyYzI4NzUxMg"); |
| 56 | form.add("username", "dory"); |
| 57 | form.add("password", "password"); |
| margaretha | a452c5e | 2018-04-25 22:48:09 +0200 | [diff] [blame] | 58 | |
| margaretha | fb027f9 | 2018-04-23 20:00:13 +0200 | [diff] [blame] | 59 | ClientResponse response = requestAuthorizationConfidentialClient(form); |
| 60 | |
| 61 | assertEquals(Status.TEMPORARY_REDIRECT.getStatusCode(), |
| 62 | response.getStatus()); |
| 63 | URI redirectUri = response.getLocation(); |
| 64 | assertTrue(redirectUri.getQuery().startsWith("code")); |
| 65 | } |
| 66 | |
| 67 | @Test |
| 68 | public void testAuthorizeInvalidRedirectUri () throws KustvaktException { |
| 69 | String redirectUri = "https://different.uri/redirect"; |
| 70 | |
| 71 | MultivaluedMap<String, String> form = new MultivaluedMapImpl(); |
| 72 | form.add("response_type", "code"); |
| 73 | form.add("client_id", "fCBbQkAyYzI4NzUxMg"); |
| 74 | form.add("username", "dory"); |
| 75 | form.add("password", "password"); |
| 76 | form.add("redirect_uri", redirectUri); |
| 77 | ClientResponse response = requestAuthorizationConfidentialClient(form); |
| 78 | |
| 79 | assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus()); |
| 80 | |
| 81 | String entity = response.getEntity(String.class); |
| 82 | JsonNode node = JsonUtils.readTree(entity); |
| 83 | assertEquals(OAuthError.CodeResponse.INVALID_REQUEST, |
| 84 | node.at("/error").asText()); |
| 85 | assertEquals(redirectUri + " is unknown", |
| 86 | node.at("/error_description").asText()); |
| 87 | } |
| 88 | |
| 89 | @Test |
| 90 | public void testAuthorizeMissingRequiredParameters () |
| 91 | throws KustvaktException { |
| 92 | MultivaluedMap<String, String> form = new MultivaluedMapImpl(); |
| 93 | // missing code |
| 94 | ClientResponse response = requestAuthorizationConfidentialClient(form); |
| 95 | |
| 96 | assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus()); |
| 97 | |
| 98 | String entity = response.getEntity(String.class); |
| 99 | JsonNode node = JsonUtils.readTree(entity); |
| 100 | assertEquals(OAuthError.CodeResponse.INVALID_REQUEST, |
| 101 | node.at("/error").asText()); |
| 102 | assertEquals("Missing response_type parameter value", |
| 103 | node.at("/error_description").asText()); |
| 104 | |
| 105 | // missing client_id |
| 106 | form.add("response_type", "code"); |
| 107 | response = requestAuthorizationConfidentialClient(form); |
| 108 | entity = response.getEntity(String.class); |
| 109 | node = JsonUtils.readTree(entity); |
| 110 | assertEquals("Missing parameters: client_id", |
| 111 | node.at("/error_description").asText()); |
| 112 | } |
| 113 | |
| 114 | @Test |
| 115 | public void testAuthorizeInvalidResponseType () throws KustvaktException { |
| 116 | MultivaluedMap<String, String> form = new MultivaluedMapImpl(); |
| 117 | form.add("response_type", "string"); |
| 118 | |
| 119 | ClientResponse response = requestAuthorizationConfidentialClient(form); |
| 120 | assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus()); |
| 121 | |
| 122 | String entity = response.getEntity(String.class); |
| 123 | JsonNode node = JsonUtils.readTree(entity); |
| 124 | assertEquals(OAuthError.CodeResponse.INVALID_REQUEST, |
| 125 | node.at("/error").asText()); |
| 126 | assertEquals("Invalid response_type parameter value", |
| 127 | node.at("/error_description").asText()); |
| 128 | } |
| 129 | |
| 130 | private ClientResponse requestToken (MultivaluedMap<String, String> form) |
| 131 | throws KustvaktException { |
| margaretha | 6374f72 | 2018-04-17 18:45:57 +0200 | [diff] [blame] | 132 | return resource().path("oauth2").path("token") |
| 133 | .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32") |
| 134 | .header(HttpHeaders.CONTENT_TYPE, |
| 135 | ContentType.APPLICATION_FORM_URLENCODED) |
| 136 | .entity(form).post(ClientResponse.class); |
| 137 | } |
| 138 | |
| 139 | @Test |
| margaretha | a452c5e | 2018-04-25 22:48:09 +0200 | [diff] [blame] | 140 | public void testRequestTokenAuthorizationConfidential () |
| 141 | throws KustvaktException { |
| 142 | |
| 143 | MultivaluedMap<String, String> authForm = new MultivaluedMapImpl(); |
| 144 | authForm.add("response_type", "code"); |
| 145 | authForm.add("client_id", "fCBbQkAyYzI4NzUxMg"); |
| 146 | authForm.add("username", "dory"); |
| 147 | authForm.add("password", "password"); |
| 148 | // form.add("scope", "read"); |
| 149 | ClientResponse response = requestAuthorizationConfidentialClient(authForm); |
| 150 | URI redirectUri = response.getLocation(); |
| 151 | String code = redirectUri.getQuery().split("=")[1]; |
| 152 | |
| 153 | MultivaluedMap<String, String> tokenForm = new MultivaluedMapImpl(); |
| 154 | tokenForm.add("grant_type", "authorization_code"); |
| 155 | tokenForm.add("client_id", "fCBbQkAyYzI4NzUxMg"); |
| 156 | tokenForm.add("client_secret", "secret"); |
| 157 | tokenForm.add("code", code); |
| 158 | |
| 159 | response = requestToken(tokenForm); |
| 160 | String entity = response.getEntity(String.class); |
| 161 | JsonNode node = JsonUtils.readTree(entity); |
| 162 | assertNotNull(node.at("/access_token").asText()); |
| 163 | assertNotNull(node.at("/refresh_token").asText()); |
| 164 | assertEquals(TokenType.BEARER.toString(), |
| 165 | node.at("/token_type").asText()); |
| 166 | assertNotNull(node.at("/expires_in").asText()); |
| 167 | } |
| 168 | |
| 169 | @Test |
| margaretha | 6374f72 | 2018-04-17 18:45:57 +0200 | [diff] [blame] | 170 | public void testRequestTokenPasswordGrantConfidential () |
| margaretha | fb027f9 | 2018-04-23 20:00:13 +0200 | [diff] [blame] | 171 | throws KustvaktException { |
| margaretha | 6374f72 | 2018-04-17 18:45:57 +0200 | [diff] [blame] | 172 | MultivaluedMap<String, String> form = new MultivaluedMapImpl(); |
| 173 | form.add("grant_type", "password"); |
| margaretha | fb027f9 | 2018-04-23 20:00:13 +0200 | [diff] [blame] | 174 | form.add("client_id", "fCBbQkAyYzI4NzUxMg"); |
| 175 | form.add("client_secret", "secret"); |
| 176 | form.add("username", "dory"); |
| 177 | form.add("password", "password"); |
| margaretha | 6374f72 | 2018-04-17 18:45:57 +0200 | [diff] [blame] | 178 | |
| margaretha | fb027f9 | 2018-04-23 20:00:13 +0200 | [diff] [blame] | 179 | ClientResponse response = requestToken(form); |
| margaretha | 6374f72 | 2018-04-17 18:45:57 +0200 | [diff] [blame] | 180 | String entity = response.getEntity(String.class); |
| 181 | |
| 182 | JsonNode node = JsonUtils.readTree(entity); |
| 183 | assertNotNull(node.at("/access_token").asText()); |
| 184 | assertNotNull(node.at("/refresh_token").asText()); |
| 185 | assertEquals(TokenType.BEARER.toString(), |
| 186 | node.at("/token_type").asText()); |
| 187 | assertNotNull(node.at("/expires_in").asText()); |
| 188 | } |
| 189 | |
| 190 | @Test |
| margaretha | fb027f9 | 2018-04-23 20:00:13 +0200 | [diff] [blame] | 191 | public void testRequestTokenPasswordGrantMissingClientSecret () |
| 192 | throws KustvaktException { |
| margaretha | 6374f72 | 2018-04-17 18:45:57 +0200 | [diff] [blame] | 193 | |
| 194 | MultivaluedMap<String, String> form = new MultivaluedMapImpl(); |
| 195 | form.add("grant_type", "password"); |
| margaretha | fb027f9 | 2018-04-23 20:00:13 +0200 | [diff] [blame] | 196 | form.add("username", "dory"); |
| 197 | form.add("password", "password"); |
| 198 | form.add("client_id", "fCBbQkAyYzI4NzUxMg"); |
| margaretha | 6374f72 | 2018-04-17 18:45:57 +0200 | [diff] [blame] | 199 | |
| margaretha | fb027f9 | 2018-04-23 20:00:13 +0200 | [diff] [blame] | 200 | ClientResponse response = requestToken(form); |
| 201 | assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus()); |
| margaretha | 6374f72 | 2018-04-17 18:45:57 +0200 | [diff] [blame] | 202 | |
| 203 | String entity = response.getEntity(String.class); |
| 204 | JsonNode node = JsonUtils.readTree(entity); |
| margaretha | fb027f9 | 2018-04-23 20:00:13 +0200 | [diff] [blame] | 205 | assertEquals(OAuthError.TokenResponse.INVALID_REQUEST, |
| margaretha | 6374f72 | 2018-04-17 18:45:57 +0200 | [diff] [blame] | 206 | node.at("/error").asText()); |
| margaretha | fb027f9 | 2018-04-23 20:00:13 +0200 | [diff] [blame] | 207 | assertEquals("Missing parameters: client_secret", |
| 208 | node.at("/error_description").asText()); |
| margaretha | 6374f72 | 2018-04-17 18:45:57 +0200 | [diff] [blame] | 209 | } |
| 210 | |
| 211 | @Test |
| 212 | public void testRequestTokenPasswordGrantMissingClientId () |
| margaretha | fb027f9 | 2018-04-23 20:00:13 +0200 | [diff] [blame] | 213 | throws KustvaktException { |
| margaretha | 6374f72 | 2018-04-17 18:45:57 +0200 | [diff] [blame] | 214 | MultivaluedMap<String, String> form = new MultivaluedMapImpl(); |
| 215 | form.add("grant_type", "password"); |
| margaretha | fb027f9 | 2018-04-23 20:00:13 +0200 | [diff] [blame] | 216 | form.add("username", "dory"); |
| 217 | form.add("password", "password"); |
| 218 | form.add("client_secret", "secret"); |
| margaretha | 6374f72 | 2018-04-17 18:45:57 +0200 | [diff] [blame] | 219 | |
| margaretha | fb027f9 | 2018-04-23 20:00:13 +0200 | [diff] [blame] | 220 | ClientResponse response = requestToken(form); |
| margaretha | 6374f72 | 2018-04-17 18:45:57 +0200 | [diff] [blame] | 221 | String entity = response.getEntity(String.class); |
| 222 | assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus()); |
| 223 | |
| 224 | JsonNode node = JsonUtils.readTree(entity); |
| 225 | assertEquals(OAuthError.TokenResponse.INVALID_REQUEST, |
| 226 | node.at("/error").asText()); |
| margaretha | fb027f9 | 2018-04-23 20:00:13 +0200 | [diff] [blame] | 227 | assertEquals("Missing parameters: client_id", |
| margaretha | 6374f72 | 2018-04-17 18:45:57 +0200 | [diff] [blame] | 228 | node.at("/error_description").asText()); |
| 229 | } |
| margaretha | fb027f9 | 2018-04-23 20:00:13 +0200 | [diff] [blame] | 230 | |
| 231 | @Test |
| 232 | public void testRequestTokenPasswordGrantPublic () |
| 233 | throws KustvaktException { |
| 234 | MultivaluedMap<String, String> form = new MultivaluedMapImpl(); |
| 235 | form.add("grant_type", "password"); |
| 236 | form.add("username", "dory"); |
| 237 | form.add("password", "password"); |
| 238 | form.add("client_id", "iBr3LsTCxOj7D2o0A5m"); |
| 239 | |
| 240 | ClientResponse response = requestToken(form); |
| 241 | String entity = response.getEntity(String.class); |
| 242 | |
| 243 | JsonNode node = JsonUtils.readTree(entity); |
| 244 | assertNotNull(node.at("/access_token").asText()); |
| 245 | assertNotNull(node.at("/refresh_token").asText()); |
| 246 | assertEquals(TokenType.BEARER.toString(), |
| 247 | node.at("/token_type").asText()); |
| 248 | assertNotNull(node.at("/expires_in").asText()); |
| 249 | } |
| margaretha | 6374f72 | 2018-04-17 18:45:57 +0200 | [diff] [blame] | 250 | |
| 251 | @Test |
| 252 | public void testRequestTokenPasswordGrantNonNative () |
| margaretha | fb027f9 | 2018-04-23 20:00:13 +0200 | [diff] [blame] | 253 | throws KustvaktException { |
| margaretha | 6374f72 | 2018-04-17 18:45:57 +0200 | [diff] [blame] | 254 | MultivaluedMap<String, String> form = new MultivaluedMapImpl(); |
| 255 | form.add("grant_type", "password"); |
| margaretha | fb027f9 | 2018-04-23 20:00:13 +0200 | [diff] [blame] | 256 | form.add("username", "dory"); |
| 257 | form.add("password", "password"); |
| 258 | // confidential nonnative |
| 259 | form.add("client_id", "9aHsGW6QflV13ixNpez"); |
| 260 | form.add("client_secret", "secret"); |
| margaretha | 6374f72 | 2018-04-17 18:45:57 +0200 | [diff] [blame] | 261 | |
| margaretha | fb027f9 | 2018-04-23 20:00:13 +0200 | [diff] [blame] | 262 | ClientResponse response = requestToken(form); |
| margaretha | 6374f72 | 2018-04-17 18:45:57 +0200 | [diff] [blame] | 263 | String entity = response.getEntity(String.class); |
| margaretha | fb027f9 | 2018-04-23 20:00:13 +0200 | [diff] [blame] | 264 | assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus()); |
| margaretha | 6374f72 | 2018-04-17 18:45:57 +0200 | [diff] [blame] | 265 | |
| 266 | JsonNode node = JsonUtils.readTree(entity); |
| 267 | assertEquals(OAuthError.TokenResponse.UNAUTHORIZED_CLIENT, |
| 268 | node.at("/error").asText()); |
| 269 | assertEquals("Password grant is not allowed for third party clients", |
| 270 | node.at("/error_description").asText()); |
| 271 | } |
| 272 | |
| margaretha | f839dde | 2018-04-16 17:52:57 +0200 | [diff] [blame] | 273 | @Test |
| 274 | public void testRequestTokenClientCredentialsGrant () |
| margaretha | fb027f9 | 2018-04-23 20:00:13 +0200 | [diff] [blame] | 275 | throws KustvaktException { |
| margaretha | f839dde | 2018-04-16 17:52:57 +0200 | [diff] [blame] | 276 | |
| 277 | MultivaluedMap<String, String> form = new MultivaluedMapImpl(); |
| 278 | form.add("grant_type", "client_credentials"); |
| margaretha | fb027f9 | 2018-04-23 20:00:13 +0200 | [diff] [blame] | 279 | form.add("client_id", "fCBbQkAyYzI4NzUxMg"); |
| 280 | form.add("client_secret", "secret"); |
| 281 | ClientResponse response = requestToken(form); |
| margaretha | f839dde | 2018-04-16 17:52:57 +0200 | [diff] [blame] | 282 | String entity = response.getEntity(String.class); |
| 283 | assertEquals(Status.OK.getStatusCode(), response.getStatus()); |
| 284 | |
| 285 | JsonNode node = JsonUtils.readTree(entity); |
| 286 | // length? |
| 287 | assertNotNull(node.at("/access_token").asText()); |
| 288 | assertNotNull(node.at("/refresh_token").asText()); |
| 289 | assertEquals(TokenType.BEARER.toString(), |
| 290 | node.at("/token_type").asText()); |
| 291 | assertNotNull(node.at("/expires_in").asText()); |
| 292 | } |
| 293 | |
| 294 | @Test |
| margaretha | fb027f9 | 2018-04-23 20:00:13 +0200 | [diff] [blame] | 295 | public void testRequestTokenMissingGrantType () throws KustvaktException { |
| margaretha | f839dde | 2018-04-16 17:52:57 +0200 | [diff] [blame] | 296 | MultivaluedMap<String, String> form = new MultivaluedMapImpl(); |
| margaretha | fb027f9 | 2018-04-23 20:00:13 +0200 | [diff] [blame] | 297 | ClientResponse response = requestToken(form); |
| margaretha | f839dde | 2018-04-16 17:52:57 +0200 | [diff] [blame] | 298 | assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus()); |
| 299 | |
| 300 | String entity = response.getEntity(String.class); |
| 301 | JsonNode node = JsonUtils.readTree(entity); |
| 302 | assertEquals(OAuthError.TokenResponse.INVALID_REQUEST, |
| 303 | node.at("/error").asText()); |
| 304 | } |
| margaretha | a048627 | 2018-04-12 19:59:31 +0200 | [diff] [blame] | 305 | |
| 306 | @Test |
| margaretha | fb027f9 | 2018-04-23 20:00:13 +0200 | [diff] [blame] | 307 | public void testRequestTokenUnsupportedGrant () throws KustvaktException { |
| margaretha | a048627 | 2018-04-12 19:59:31 +0200 | [diff] [blame] | 308 | |
| 309 | MultivaluedMap<String, String> form = new MultivaluedMapImpl(); |
| margaretha | 0512231 | 2018-04-16 15:01:34 +0200 | [diff] [blame] | 310 | form.add("grant_type", "blahblah"); |
| 311 | |
| margaretha | a048627 | 2018-04-12 19:59:31 +0200 | [diff] [blame] | 312 | ClientResponse response = resource().path("oauth2").path("token") |
| margaretha | a048627 | 2018-04-12 19:59:31 +0200 | [diff] [blame] | 313 | .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32") |
| 314 | .header(HttpHeaders.CONTENT_TYPE, |
| 315 | ContentType.APPLICATION_FORM_URLENCODED) |
| 316 | .entity(form).post(ClientResponse.class); |
| 317 | |
| margaretha | 0512231 | 2018-04-16 15:01:34 +0200 | [diff] [blame] | 318 | String entity = response.getEntity(String.class); |
| margaretha | 0512231 | 2018-04-16 15:01:34 +0200 | [diff] [blame] | 319 | assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus()); |
| 320 | |
| 321 | JsonNode node = JsonUtils.readTree(entity); |
| margaretha | f839dde | 2018-04-16 17:52:57 +0200 | [diff] [blame] | 322 | assertEquals("Invalid grant_type parameter value", |
| margaretha | 0512231 | 2018-04-16 15:01:34 +0200 | [diff] [blame] | 323 | node.get("error_description").asText()); |
| margaretha | f839dde | 2018-04-16 17:52:57 +0200 | [diff] [blame] | 324 | assertEquals(OAuthError.TokenResponse.INVALID_REQUEST, |
| 325 | node.get("error").asText()); |
| margaretha | a048627 | 2018-04-12 19:59:31 +0200 | [diff] [blame] | 326 | } |
| 327 | |
| 328 | } |