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