1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.wifi; 18 19 import android.annotation.IntDef; 20 import android.annotation.NonNull; 21 import android.net.wifi.ScanResult; 22 import android.os.Handler; 23 import android.util.Log; 24 25 import com.android.libraries.entitlement.ServiceEntitlementException; 26 import com.android.server.wifi.entitlement.http.HttpClient; 27 import com.android.server.wifi.entitlement.http.HttpConstants.RequestMethod; 28 import com.android.server.wifi.entitlement.http.HttpRequest; 29 import com.android.server.wifi.entitlement.http.HttpResponse; 30 31 import org.json.JSONArray; 32 import org.json.JSONException; 33 import org.json.JSONObject; 34 35 import java.lang.annotation.Retention; 36 import java.lang.annotation.RetentionPolicy; 37 import java.util.HashMap; 38 import java.util.Map; 39 40 /** 41 * Class that queries the AFC server with HTTP post requests and returns the HTTP response result. 42 */ 43 public class AfcClient { 44 private static final String TAG = "WifiAfcClient"; 45 public static final int REASON_NO_URL_SPECIFIED = 0; 46 public static final int REASON_AFC_RESPONSE_CODE_ERROR = 1; 47 public static final int REASON_SERVICE_ENTITLEMENT_FAILURE = 2; 48 public static final int REASON_JSON_FAILURE = 3; 49 public static final int REASON_UNDEFINED_FAILURE = 4; 50 @IntDef(prefix = { "REASON_" }, value = { 51 REASON_NO_URL_SPECIFIED, 52 REASON_AFC_RESPONSE_CODE_ERROR, 53 REASON_SERVICE_ENTITLEMENT_FAILURE, 54 REASON_JSON_FAILURE, 55 REASON_UNDEFINED_FAILURE, 56 }) 57 @Retention(RetentionPolicy.SOURCE) 58 public @interface FailureReasonCode {} 59 static final int CONNECT_TIMEOUT_SECS = 30; 60 static final int LOW_FREQUENCY = ScanResult.BAND_6_GHZ_START_FREQ_MHZ; 61 62 // TODO(b/291774201): Update max frequency since server is currently returning errors with 63 // higher values than 6425. 64 static final int HIGH_FREQUENCY = 6425; 65 static final String SERIAL_NUMBER = "ABCDEFG"; 66 static final String NRA = "FCC"; 67 static final String ID = "EFGHIJK"; 68 static final String RULE_SET_ID = "US_47_CFR_PART_15_SUBPART_E"; 69 static final String VERSION = "1.2"; 70 private static String sServerUrl; 71 private static HashMap<String, String> sRequestProperties; 72 private final Handler mBackgroundHandler; 73 private int mRequestId; 74 AfcClient(Handler backgroundHandler)75 AfcClient(Handler backgroundHandler) { 76 mRequestId = 0; 77 sRequestProperties = new HashMap<>(); 78 mBackgroundHandler = backgroundHandler; 79 } 80 81 /** 82 * Query the AFC server using the background thread and post the response to the callback on 83 * the Wi-Fi handler thread. 84 */ queryAfcServer(@onNull AfcLocation afcLocation, @NonNull Handler wifiHandler, @NonNull Callback callback)85 public void queryAfcServer(@NonNull AfcLocation afcLocation, @NonNull Handler wifiHandler, 86 @NonNull Callback callback) { 87 // If no URL is provided, then fail to proceed with sending request to AFC server 88 if (sServerUrl == null) { 89 wifiHandler.post(() -> callback.onFailure(REASON_NO_URL_SPECIFIED, 90 "No Server URL was provided through command line argument. Must " 91 + "provide URL through configure-afc-server wifi shell command." 92 )); 93 return; 94 } 95 96 HttpRequest httpRequest = getAfcHttpRequestObject(afcLocation); 97 mBackgroundHandler.post(() -> { 98 try { 99 HttpResponse httpResponse = HttpClient.request(httpRequest); 100 JSONObject httpResponseBodyJSON = new JSONObject(httpResponse.body()); 101 102 AfcServerResponse serverResponse = AfcServerResponse 103 .fromSpectrumInquiryResponse(getHttpResponseCode(httpResponse), 104 getAvailableSpectrumInquiryResponse(httpResponseBodyJSON, 105 mRequestId)); 106 if (serverResponse == null) { 107 wifiHandler.post(() -> callback.onFailure(REASON_JSON_FAILURE, 108 "Encountered JSON error when parsing AFC server's " 109 + "response." 110 )); 111 } else if (serverResponse.getAfcChannelAllowance() == null) { 112 wifiHandler.post(() -> callback.onFailure( 113 REASON_AFC_RESPONSE_CODE_ERROR, "Response code server error. " 114 + "HttpResponseCode=" + serverResponse.getHttpResponseCode() 115 + ", AfcServerResponseCode=" + serverResponse 116 .getAfcResponseCode() + ", Short Description: " 117 + serverResponse.getAfcResponseDescription())); 118 } else { 119 // Post the server response to the callback 120 wifiHandler.post(() -> callback.onResult(serverResponse, afcLocation)); 121 } 122 } catch (ServiceEntitlementException e) { 123 wifiHandler.post(() -> callback.onFailure(REASON_SERVICE_ENTITLEMENT_FAILURE, 124 "Encountered Service Entitlement Exception when sending " 125 + "request to server. " + e)); 126 } catch (JSONException e) { 127 wifiHandler.post(() -> callback.onFailure(REASON_JSON_FAILURE, 128 "Encountered JSON error when parsing HTTP response." + e 129 )); 130 } catch (Exception e) { 131 wifiHandler.post(() -> callback.onFailure(REASON_UNDEFINED_FAILURE, 132 "Encountered unexpected error when parsing AFC server's response." 133 + e)); 134 } finally { 135 // Increment the request ID by 1 for the next request 136 mRequestId++; 137 } 138 }); 139 } 140 141 /** 142 * Create and return the HttpRequest object that queries the AFC server for the afcLocation 143 * object. 144 */ getAfcHttpRequestObject(AfcLocation afcLocation)145 public HttpRequest getAfcHttpRequestObject(AfcLocation afcLocation) { 146 HttpRequest.Builder httpRequestBuilder = HttpRequest.builder() 147 .setUrl(sServerUrl) 148 .setRequestMethod(RequestMethod.POST) 149 .setTimeoutInSec(CONNECT_TIMEOUT_SECS); 150 for (Map.Entry<String, String> requestProperty : sRequestProperties.entrySet()) { 151 httpRequestBuilder.addRequestProperty(requestProperty.getKey(), 152 requestProperty.getValue()); 153 } 154 JSONObject jsonRequestObject = getAfcRequestJSONObject(afcLocation); 155 httpRequestBuilder.setPostData(jsonRequestObject); 156 return httpRequestBuilder.build(); 157 } 158 159 /** 160 * Get the AFC request JSON object used to query the AFC server. 161 */ getAfcRequestJSONObject(AfcLocation afcLocation)162 private JSONObject getAfcRequestJSONObject(AfcLocation afcLocation) { 163 try { 164 JSONObject requestObject = new JSONObject(); 165 JSONArray inquiryRequests = new JSONArray(); 166 JSONObject inquiryRequest = new JSONObject(); 167 168 inquiryRequest.put("requestId", String.valueOf(mRequestId)); 169 170 JSONObject deviceDescriptor = new JSONObject(); 171 deviceDescriptor.put("serialNumber", AfcClient.SERIAL_NUMBER); 172 JSONObject certificationId = new JSONObject(); 173 certificationId.put("nra", AfcClient.NRA); 174 certificationId.put("id", AfcClient.ID); 175 deviceDescriptor.put("certificationId", certificationId); 176 deviceDescriptor.put("rulesetIds", AfcClient.RULE_SET_ID); 177 inquiryRequest.put("deviceDescriptor", deviceDescriptor); 178 179 JSONObject location = afcLocation.toJson(); 180 inquiryRequest.put("location", location); 181 182 JSONArray inquiredFrequencyRange = new JSONArray(); 183 JSONObject range = new JSONObject(); 184 range.put("lowFrequency", AfcClient.LOW_FREQUENCY); 185 range.put("highFrequency", AfcClient.HIGH_FREQUENCY); 186 inquiredFrequencyRange.put(range); 187 inquiryRequest.put("inquiredFrequencyRange", inquiredFrequencyRange); 188 189 inquiryRequests.put(0, inquiryRequest); 190 191 requestObject.put("version", AfcClient.VERSION); 192 requestObject.put("availableSpectrumInquiryRequests", inquiryRequests); 193 194 return requestObject; 195 } catch (JSONException e) { 196 Log.e(TAG, "Encountered error when building JSON object: " + e); 197 return null; 198 } 199 } 200 201 /** 202 * Parses the AFC server's HTTP response and finds an Available Spectrum Inquiry Response with 203 * a matching request ID. 204 * 205 * @param httpResponseJSON the response of the AFC server 206 * @return the Available Spectrum Inquiry Response as a JSON Object with a request ID matching 207 * the ID used in the request, or null if no object with a matching ID is found. 208 */ getAvailableSpectrumInquiryResponse(JSONObject httpResponseJSON, int requestId)209 private JSONObject getAvailableSpectrumInquiryResponse(JSONObject httpResponseJSON, 210 int requestId) { 211 if (httpResponseJSON == null) { 212 return null; 213 } 214 String requestIdString = Integer.toString(requestId); 215 216 try { 217 JSONArray spectrumInquiryResponses = httpResponseJSON.getJSONArray( 218 "availableSpectrumInquiryResponses"); 219 220 // iterate through responses to find one with a matching request ID 221 for (int i = 0, numResponses = spectrumInquiryResponses.length(); 222 i < numResponses; i++) { 223 JSONObject spectrumInquiryResponse = spectrumInquiryResponses.getJSONObject(i); 224 if (requestIdString.equals(spectrumInquiryResponse.getString("requestId"))) { 225 return spectrumInquiryResponse; 226 } 227 } 228 229 Log.e(TAG, "Did not find an available spectrum inquiry response with request ID: " 230 + requestIdString); 231 } catch (JSONException e) { 232 Log.e(TAG, "Error occurred while parsing available spectrum inquiry response: " 233 + e); 234 } 235 return null; 236 } 237 238 /** 239 * @return the http response code, or 500 if it does not exist 240 */ getHttpResponseCode(HttpResponse httpResponse)241 private int getHttpResponseCode(HttpResponse httpResponse) { 242 return httpResponse == null ? 500 : httpResponse.responseCode(); 243 } 244 245 /** 246 * Set the server URL used to query the AFC server. 247 */ setServerURL(String url)248 public void setServerURL(String url) { 249 sServerUrl = url; 250 } 251 252 /** 253 * Sets the request properties Map through copying the Map created from the configure-afc-server 254 * wifi-shell command. 255 * @param requestProperties A map with key and value Strings for the HTTP header's request 256 * property fields. Each key has a corresponding value. 257 */ setRequestPropertyPairs(Map<String, String> requestProperties)258 public void setRequestPropertyPairs(Map<String, String> requestProperties) { 259 sRequestProperties = new HashMap<>(); 260 sRequestProperties.putAll(requestProperties); 261 } 262 263 /** 264 * Get the server URL for this AfcClient. 265 */ getServerURL()266 public String getServerURL() { 267 return sServerUrl; 268 } 269 270 /** 271 * Get the Map with <key, value> header fields for the HTTP request properties. 272 */ getRequestProperties()273 public Map<String, String> getRequestProperties() { 274 return sRequestProperties; 275 } 276 277 /** 278 * Callback which will be called after AfcResponse retrieval. 279 */ 280 public interface Callback { 281 /** 282 * Indicates that an AfcServerResponse was received successfully. 283 */ onResult(AfcServerResponse serverResponse, AfcLocation afcLocation)284 void onResult(AfcServerResponse serverResponse, AfcLocation afcLocation); 285 /** 286 * Indicate a failure happens when receiving the AfcServerResponse. 287 * @param reasonCode The failure reason code. 288 * @param description The description of the failure. 289 */ onFailure(@ailureReasonCode int reasonCode, String description)290 void onFailure(@FailureReasonCode int reasonCode, String description); 291 } 292 } 293