Browse Source

[efi] Provide access to files stored on EFI filesystems

Provide access to local files via the "file://" URI scheme.  There are
three syntaxes:

  - An opaque URI with a relative path (e.g. "file:script.ipxe").
    This will be interpreted as a path relative to the iPXE binary.

  - A hierarchical URI with a non-network absolute path
    (e.g. "file:/boot/script.ipxe").  This will be interpreted as a
    path relative to the root of the filesystem from which the iPXE
    binary was loaded.

  - A hierarchical URI with a network path in which the authority is a
    volume label (e.g. "file://bootdisk/script.ipxe").  This will be
    interpreted as a path relative to the root of the filesystem with
    the specified volume label.

Note that the potentially desirable shell mappings (e.g. "fs0:" and
"blk0:") are concepts internal to the UEFI shell binary, and do not
seem to be exposed in any way to external executables.  The old
EFI_SHELL_PROTOCOL (which did provide access to these mappings) is no
longer installed by current versions of the UEFI shell.

Signed-off-by: Michael Brown <mcb30@ipxe.org>
tags/v1.20.1
Michael Brown 8 years ago
parent
commit
9913a405ea

+ 4
- 0
src/config/config_efi.c View File

@@ -21,6 +21,7 @@
21 21
 
22 22
 FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL );
23 23
 
24
+#include <config/general.h>
24 25
 #include <config/console.h>
25 26
 
26 27
 /** @file
@@ -45,3 +46,6 @@ REQUIRE_OBJECT ( efi_fbcon );
45 46
 #ifdef CONSOLE_FRAMEBUFFER
46 47
 REQUIRE_OBJECT ( efi_fbcon );
47 48
 #endif
49
+#ifdef DOWNLOAD_PROTO_FILE
50
+REQUIRE_OBJECT ( efi_local );
51
+#endif

+ 2
- 0
src/config/defaults/efi.h View File

@@ -24,6 +24,8 @@ FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL );
24 24
 #define TIME_EFI
25 25
 #define REBOOT_EFI
26 26
 
27
+#define DOWNLOAD_PROTO_FILE	/* Local filesystem access */
28
+
27 29
 #define	IMAGE_EFI		/* EFI image support */
28 30
 #define	IMAGE_SCRIPT		/* iPXE script image support */
29 31
 

+ 1
- 0
src/config/general.h View File

@@ -57,6 +57,7 @@ FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL );
57 57
 #undef	DOWNLOAD_PROTO_FTP	/* File Transfer Protocol */
58 58
 #undef	DOWNLOAD_PROTO_SLAM	/* Scalable Local Area Multicast */
59 59
 #undef	DOWNLOAD_PROTO_NFS	/* Network File System Protocol */
60
+//#undef DOWNLOAD_PROTO_FILE	/* Local filesystem access */
60 61
 
61 62
 /*
62 63
  * SAN boot protocols

+ 1
- 0
src/include/ipxe/errfile.h View File

@@ -347,6 +347,7 @@ FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL );
347 347
 #define ERRFILE_efi_pxe		      ( ERRFILE_OTHER | 0x004a0000 )
348 348
 #define ERRFILE_efi_usb		      ( ERRFILE_OTHER | 0x004b0000 )
349 349
 #define ERRFILE_efi_fbcon	      ( ERRFILE_OTHER | 0x004c0000 )
350
+#define ERRFILE_efi_local	      ( ERRFILE_OTHER | 0x004d0000 )
350 351
 
351 352
 /** @} */
352 353
 

+ 573
- 0
src/interface/efi/efi_local.c View File

@@ -0,0 +1,573 @@
1
+/*
2
+ * Copyright (C) 2016 Michael Brown <mbrown@fensystems.co.uk>.
3
+ *
4
+ * This program is free software; you can redistribute it and/or
5
+ * modify it under the terms of the GNU General Public License as
6
+ * published by the Free Software Foundation; either version 2 of the
7
+ * License, or (at your option) any later version.
8
+ *
9
+ * This program is distributed in the hope that it will be useful, but
10
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12
+ * General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU General Public License
15
+ * along with this program; if not, write to the Free Software
16
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17
+ * 02110-1301, USA.
18
+ *
19
+ * You can also choose to distribute this program under the terms of
20
+ * the Unmodified Binary Distribution Licence (as given in the file
21
+ * COPYING.UBDL), provided that you have satisfied its requirements.
22
+ */
23
+
24
+FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL );
25
+
26
+#include <string.h>
27
+#include <strings.h>
28
+#include <stdio.h>
29
+#include <errno.h>
30
+#include <assert.h>
31
+#include <ipxe/refcnt.h>
32
+#include <ipxe/malloc.h>
33
+#include <ipxe/xfer.h>
34
+#include <ipxe/open.h>
35
+#include <ipxe/uri.h>
36
+#include <ipxe/iobuf.h>
37
+#include <ipxe/process.h>
38
+#include <ipxe/efi/efi.h>
39
+#include <ipxe/efi/efi_strings.h>
40
+#include <ipxe/efi/efi_utils.h>
41
+#include <ipxe/efi/Protocol/SimpleFileSystem.h>
42
+#include <ipxe/efi/Guid/FileInfo.h>
43
+#include <ipxe/efi/Guid/FileSystemInfo.h>
44
+
45
+/** @file
46
+ *
47
+ * EFI local file access
48
+ *
49
+ */
50
+
51
+/** Download blocksize */
52
+#define EFI_LOCAL_BLKSIZE 4096
53
+
54
+/** An EFI local file */
55
+struct efi_local {
56
+	/** Reference count */
57
+	struct refcnt refcnt;
58
+	/** Data transfer interface */
59
+	struct interface xfer;
60
+	/** Download process */
61
+	struct process process;
62
+
63
+	/** EFI root directory */
64
+	EFI_FILE_PROTOCOL *root;
65
+	/** EFI file */
66
+	EFI_FILE_PROTOCOL *file;
67
+	/** Length of file */
68
+	size_t len;
69
+};
70
+
71
+/**
72
+ * Close local file
73
+ *
74
+ * @v local		Local file
75
+ * @v rc		Reason for close
76
+ */
77
+static void efi_local_close ( struct efi_local *local, int rc ) {
78
+
79
+	/* Stop process */
80
+	process_del ( &local->process );
81
+
82
+	/* Shut down data transfer interface */
83
+	intf_shutdown ( &local->xfer, rc );
84
+
85
+	/* Close EFI file */
86
+	if ( local->file ) {
87
+		local->file->Close ( local->file );
88
+		local->file = NULL;
89
+	}
90
+
91
+	/* Close EFI root directory */
92
+	if ( local->root ) {
93
+		local->root->Close ( local->root );
94
+		local->root = NULL;
95
+	}
96
+}
97
+
98
+/**
99
+ * Local file process
100
+ *
101
+ * @v local		Local file
102
+ */
103
+static void efi_local_step ( struct efi_local *local ) {
104
+	EFI_FILE_PROTOCOL *file = local->file;
105
+	struct io_buffer *iobuf = NULL;
106
+	size_t remaining;
107
+	size_t frag_len;
108
+	UINTN size;
109
+	EFI_STATUS efirc;
110
+	int rc;
111
+
112
+	/* Wait until data transfer interface is ready */
113
+	if ( ! xfer_window ( &local->xfer ) )
114
+		return;
115
+
116
+	/* Presize receive buffer */
117
+	remaining = local->len;
118
+	xfer_seek ( &local->xfer, remaining );
119
+	xfer_seek ( &local->xfer, 0 );
120
+
121
+	/* Get file contents */
122
+	while ( remaining ) {
123
+
124
+		/* Calculate length for this fragment */
125
+		frag_len = remaining;
126
+		if ( frag_len > EFI_LOCAL_BLKSIZE )
127
+			frag_len = EFI_LOCAL_BLKSIZE;
128
+
129
+		/* Allocate I/O buffer */
130
+		iobuf = xfer_alloc_iob ( &local->xfer, frag_len );
131
+		if ( ! iobuf ) {
132
+			rc = -ENOMEM;
133
+			goto err;
134
+		}
135
+
136
+		/* Read block */
137
+		size = frag_len;
138
+		if ( ( efirc = file->Read ( file, &size, iobuf->data ) ) != 0 ){
139
+			rc = -EEFI ( efirc );
140
+			DBGC ( local, "LOCAL %p could not read from file: %s\n",
141
+			       local, strerror ( rc ) );
142
+			goto err;
143
+		}
144
+		assert ( size <= frag_len );
145
+		iob_put ( iobuf, size );
146
+
147
+		/* Deliver data */
148
+		if ( ( rc = xfer_deliver_iob ( &local->xfer,
149
+					       iob_disown ( iobuf ) ) ) != 0 ) {
150
+			DBGC ( local, "LOCAL %p could not deliver data: %s\n",
151
+			       local, strerror ( rc ) );
152
+			goto err;
153
+		}
154
+
155
+		/* Move to next block */
156
+		remaining -= frag_len;
157
+	}
158
+
159
+	/* Close download */
160
+	efi_local_close ( local, 0 );
161
+
162
+	return;
163
+
164
+ err:
165
+	free_iob ( iobuf );
166
+	efi_local_close ( local, rc );
167
+}
168
+
169
+/** Data transfer interface operations */
170
+static struct interface_operation efi_local_operations[] = {
171
+	INTF_OP ( xfer_window_changed, struct efi_local *, efi_local_step ),
172
+	INTF_OP ( intf_close, struct efi_local *, efi_local_close ),
173
+};
174
+
175
+/** Data transfer interface descriptor */
176
+static struct interface_descriptor efi_local_xfer_desc =
177
+	INTF_DESC ( struct efi_local, xfer, efi_local_operations );
178
+
179
+/** Process descriptor */
180
+static struct process_descriptor efi_local_process_desc =
181
+	PROC_DESC_ONCE ( struct efi_local, process, efi_local_step );
182
+
183
+/**
184
+ * Check for matching volume name
185
+ *
186
+ * @v local		Local file
187
+ * @v device		Device handle
188
+ * @v root		Root filesystem handle
189
+ * @v volume		Volume name
190
+ * @ret rc		Return status code
191
+ */
192
+static int efi_local_check_volume_name ( struct efi_local *local,
193
+					 EFI_HANDLE device,
194
+					 EFI_FILE_PROTOCOL *root,
195
+					 const char *volume ) {
196
+	EFI_FILE_SYSTEM_INFO *info;
197
+	UINTN size;
198
+	char *label;
199
+	EFI_STATUS efirc;
200
+	int rc;
201
+
202
+	/* Get length of file system information */
203
+	size = 0;
204
+	root->GetInfo ( root, &efi_file_system_info_id, &size, NULL );
205
+
206
+	/* Allocate file system information */
207
+	info = malloc ( size );
208
+	if ( ! info ) {
209
+		rc = -ENOMEM;
210
+		goto err_alloc_info;
211
+	}
212
+
213
+	/* Get file system information */
214
+	if ( ( efirc = root->GetInfo ( root, &efi_file_system_info_id, &size,
215
+				       info ) ) != 0 ) {
216
+		rc = -EEFI ( efirc );
217
+		DBGC ( local, "LOCAL %p could not get file system info on %s: "
218
+		       "%s\n", local, efi_handle_name ( device ),
219
+		       strerror ( rc ) );
220
+		goto err_get_info;
221
+	}
222
+	DBGC2 ( local, "LOCAL %p found %s with label \"%ls\"\n",
223
+		local, efi_handle_name ( device ), info->VolumeLabel );
224
+
225
+	/* Construct volume label for comparison */
226
+	if ( asprintf ( &label, "%ls", info->VolumeLabel ) < 0 ) {
227
+		rc = -ENOMEM;
228
+		goto err_alloc_label;
229
+	}
230
+
231
+	/* Compare volume label */
232
+	if ( strcasecmp ( volume, label ) != 0 ) {
233
+		rc = -ENOENT;
234
+		goto err_compare;
235
+	}
236
+
237
+	/* Success */
238
+	rc = 0;
239
+
240
+ err_compare:
241
+	free ( label );
242
+ err_alloc_label:
243
+ err_get_info:
244
+	free ( info );
245
+ err_alloc_info:
246
+	return rc;
247
+}
248
+
249
+/**
250
+ * Open root filesystem
251
+ *
252
+ * @v local		Local file
253
+ * @v device		Device handle
254
+ * @v root		Root filesystem handle to fill in
255
+ * @ret rc		Return status code
256
+ */
257
+static int efi_local_open_root ( struct efi_local *local, EFI_HANDLE device,
258
+				 EFI_FILE_PROTOCOL **root ) {
259
+	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
260
+	union {
261
+		void *interface;
262
+		EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *fs;
263
+	} u;
264
+	EFI_STATUS efirc;
265
+	int rc;
266
+
267
+	/* Open file system protocol */
268
+	if ( ( efirc = bs->OpenProtocol ( device,
269
+					  &efi_simple_file_system_protocol_guid,
270
+					  &u.interface, efi_image_handle,
271
+					  device,
272
+					  EFI_OPEN_PROTOCOL_GET_PROTOCOL ))!=0){
273
+		rc = -EEFI ( efirc );
274
+		DBGC ( local, "LOCAL %p could not open filesystem on %s: %s\n",
275
+		       local, efi_handle_name ( device ), strerror ( rc ) );
276
+		goto err_filesystem;
277
+	}
278
+
279
+	/* Open root directory */
280
+	if ( ( efirc = u.fs->OpenVolume ( u.fs, root ) ) != 0 ) {
281
+		rc = -EEFI ( efirc );
282
+		DBGC ( local, "LOCAL %p could not open volume on %s: %s\n",
283
+		       local, efi_handle_name ( device ), strerror ( rc ) );
284
+		goto err_volume;
285
+	}
286
+
287
+	/* Success */
288
+	rc = 0;
289
+
290
+ err_volume:
291
+	bs->CloseProtocol ( device, &efi_simple_file_system_protocol_guid,
292
+			    efi_image_handle, device );
293
+ err_filesystem:
294
+	return rc;
295
+}
296
+
297
+/**
298
+ * Open root filesystem of specified volume
299
+ *
300
+ * @v local		Local file
301
+ * @v volume		Volume name, or NULL to use loaded image's device
302
+ * @ret rc		Return status code
303
+ */
304
+static int efi_local_open_volume ( struct efi_local *local,
305
+				   const char *volume ) {
306
+	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
307
+	EFI_GUID *protocol = &efi_simple_file_system_protocol_guid;
308
+	int ( * check ) ( struct efi_local *local, EFI_HANDLE device,
309
+			  EFI_FILE_PROTOCOL *root, const char *volume );
310
+	EFI_FILE_PROTOCOL *root;
311
+	EFI_HANDLE *handles;
312
+	EFI_HANDLE device;
313
+	UINTN num_handles;
314
+	UINTN i;
315
+	EFI_STATUS efirc;
316
+	int rc;
317
+
318
+	/* Identify candidate handles */
319
+	if ( volume ) {
320
+		/* Locate all filesystem handles */
321
+		if ( ( efirc = bs->LocateHandleBuffer ( ByProtocol, protocol,
322
+							NULL, &num_handles,
323
+							&handles ) ) != 0 ) {
324
+			rc = -EEFI ( efirc );
325
+			DBGC ( local, "LOCAL %p could not enumerate handles: "
326
+			       "%s\n", local, strerror ( rc ) );
327
+			return rc;
328
+		}
329
+		check = efi_local_check_volume_name;
330
+	} else {
331
+		/* Use our loaded image's device handle */
332
+		handles = &efi_loaded_image->DeviceHandle;
333
+		num_handles = 1;
334
+		check = NULL;
335
+	}
336
+
337
+	/* Find matching handle */
338
+	for ( i = 0 ; i < num_handles ; i++ ) {
339
+
340
+		/* Get this device handle */
341
+		device = handles[i];
342
+
343
+		/* Open root directory */
344
+		if ( ( rc = efi_local_open_root ( local, device, &root ) ) != 0)
345
+			continue;
346
+
347
+		/* Check volume name, if applicable */
348
+		if ( ( check == NULL ) ||
349
+		     ( ( rc = check ( local, device, root, volume ) ) == 0 ) ) {
350
+			DBGC ( local, "LOCAL %p using %s",
351
+			       local, efi_handle_name ( device ) );
352
+			if ( volume )
353
+				DBGC ( local, " with label \"%s\"", volume );
354
+			DBGC ( local, "\n" );
355
+			local->root = root;
356
+			break;
357
+		}
358
+
359
+		/* Close root directory */
360
+		root->Close ( root );
361
+	}
362
+
363
+	/* Free handles, if applicable */
364
+	if ( volume )
365
+		bs->FreePool ( handles );
366
+
367
+	/* Fail if we found no matching handle */
368
+	if ( ! local->root ) {
369
+		DBGC ( local, "LOCAL %p found no matching handle\n", local );
370
+		return -ENOENT;
371
+	}
372
+
373
+	return 0;
374
+}
375
+
376
+/**
377
+ * Open fully-resolved path
378
+ *
379
+ * @v local		Local file
380
+ * @v resolved		Resolved path
381
+ * @ret rc		Return status code
382
+ */
383
+static int efi_local_open_resolved ( struct efi_local *local,
384
+				     const char *resolved ) {
385
+	size_t name_len = strlen ( resolved );
386
+	CHAR16 name[ name_len + 1 /* wNUL */ ];
387
+	EFI_FILE_PROTOCOL *file;
388
+	EFI_STATUS efirc;
389
+	int rc;
390
+
391
+	/* Construct filename */
392
+	efi_snprintf ( name, ( name_len + 1 /* wNUL */ ), "%s", resolved );
393
+
394
+	/* Open file */
395
+	if ( ( efirc = local->root->Open ( local->root, &file, name,
396
+					   EFI_FILE_MODE_READ, 0 ) ) != 0 ) {
397
+		rc = -EEFI ( efirc );
398
+		DBGC ( local, "LOCAL %p could not open \"%s\": %s\n",
399
+		       local, resolved, strerror ( rc ) );
400
+		return rc;
401
+	}
402
+	local->file = file;
403
+
404
+	return 0;
405
+}
406
+
407
+/**
408
+ * Open specified path
409
+ *
410
+ * @v local		Local file
411
+ * @v path		Path to file
412
+ * @ret rc		Return status code
413
+ */
414
+static int efi_local_open_path ( struct efi_local *local, const char *path ) {
415
+	FILEPATH_DEVICE_PATH *fp = container_of ( efi_loaded_image->FilePath,
416
+						  FILEPATH_DEVICE_PATH, Header);
417
+	size_t fp_len = ( fp ? efi_devpath_len ( &fp->Header ) : 0 );
418
+	char base[ fp_len / 2 /* Cannot exceed this length */ ];
419
+	size_t remaining = sizeof ( base );
420
+	size_t len;
421
+	char *resolved;
422
+	char *tmp;
423
+	int rc;
424
+
425
+	/* Construct base path to our own image, if possible */
426
+	memset ( base, 0, sizeof ( base ) );
427
+	tmp = base;
428
+	while ( fp && ( fp->Header.Type != END_DEVICE_PATH_TYPE ) ) {
429
+		len = snprintf ( tmp, remaining, "%ls", fp->PathName );
430
+		assert ( len < remaining );
431
+		tmp += len;
432
+		remaining -= len;
433
+		fp = ( ( ( void * ) fp ) + ( ( fp->Header.Length[1] << 8 ) |
434
+					     fp->Header.Length[0] ) );
435
+	}
436
+	DBGC2 ( local, "LOCAL %p base path \"%s\"\n",
437
+		local, base );
438
+
439
+	/* Convert to sane path separators */
440
+	for ( tmp = base ; *tmp ; tmp++ ) {
441
+		if ( *tmp == '\\' )
442
+			*tmp = '/';
443
+	}
444
+
445
+	/* Resolve path */
446
+	resolved = resolve_path ( base, path );
447
+	if ( ! resolved ) {
448
+		rc = -ENOMEM;
449
+		goto err_resolve;
450
+	}
451
+
452
+	/* Convert to insane path separators */
453
+	for ( tmp = resolved ; *tmp ; tmp++ ) {
454
+		if ( *tmp == '/' )
455
+			*tmp = '\\';
456
+	}
457
+	DBGC ( local, "LOCAL %p using \"%s\"\n",
458
+	       local, resolved );
459
+
460
+	/* Open resolved path */
461
+	if ( ( rc = efi_local_open_resolved ( local, resolved ) ) != 0 )
462
+		goto err_open;
463
+
464
+ err_open:
465
+	free ( resolved );
466
+ err_resolve:
467
+	return rc;
468
+}
469
+
470
+/**
471
+ * Get file length
472
+ *
473
+ * @v local		Local file
474
+ * @ret rc		Return status code
475
+ */
476
+static int efi_local_len ( struct efi_local *local ) {
477
+	EFI_FILE_PROTOCOL *file = local->file;
478
+	EFI_FILE_INFO *info;
479
+	EFI_STATUS efirc;
480
+	UINTN size;
481
+	int rc;
482
+
483
+	/* Get size of file information */
484
+	size = 0;
485
+	file->GetInfo ( file, &efi_file_info_id, &size, NULL );
486
+
487
+	/* Allocate file information */
488
+	info = malloc ( size );
489
+	if ( ! info ) {
490
+		rc = -ENOMEM;
491
+		goto err_alloc;
492
+	}
493
+
494
+	/* Get file information */
495
+	if ( ( efirc = file->GetInfo ( file, &efi_file_info_id, &size,
496
+				       info ) ) != 0 ) {
497
+		rc = -EEFI ( efirc );
498
+		DBGC ( local, "LOCAL %p could not get file info: %s\n",
499
+		       local, strerror ( rc ) );
500
+		goto err_info;
501
+	}
502
+
503
+	/* Record file length */
504
+	local->len = info->FileSize;
505
+
506
+	/* Success */
507
+	rc = 0;
508
+
509
+ err_info:
510
+	free ( info );
511
+ err_alloc:
512
+	return rc;
513
+}
514
+
515
+/**
516
+ * Open local file
517
+ *
518
+ * @v xfer		Data transfer interface
519
+ * @v uri		Request URI
520
+ * @ret rc		Return status code
521
+ */
522
+static int efi_local_open ( struct interface *xfer, struct uri *uri ) {
523
+	struct efi_local *local;
524
+	const char *volume;
525
+	const char *path;
526
+	int rc;
527
+
528
+	/* Parse URI */
529
+	volume = ( ( uri->host && uri->host[0] ) ? uri->host : NULL );
530
+	path = ( uri->opaque ? uri->opaque : uri->path );
531
+
532
+	/* Allocate and initialise structure */
533
+	local = zalloc ( sizeof ( *local ) );
534
+	if ( ! local ) {
535
+		rc = -ENOMEM;
536
+		goto err_alloc;
537
+	}
538
+	ref_init ( &local->refcnt, NULL );
539
+	intf_init ( &local->xfer, &efi_local_xfer_desc, &local->refcnt );
540
+	process_init ( &local->process, &efi_local_process_desc,
541
+		       &local->refcnt );
542
+
543
+	/* Open specified volume */
544
+	if ( ( rc = efi_local_open_volume ( local, volume ) ) != 0 )
545
+		goto err_open_root;
546
+
547
+	/* Open specified path */
548
+	if ( ( rc = efi_local_open_path ( local, path ) ) != 0 )
549
+		goto err_open_file;
550
+
551
+	/* Get length of file */
552
+	if ( ( rc = efi_local_len ( local ) ) != 0 )
553
+		goto err_len;
554
+
555
+	/* Attach to parent interface, mortalise self, and return */
556
+	intf_plug_plug ( &local->xfer, xfer );
557
+	ref_put ( &local->refcnt );
558
+	return 0;
559
+
560
+ err_len:
561
+ err_open_file:
562
+ err_open_root:
563
+	efi_local_close ( local, 0 );
564
+	ref_put ( &local->refcnt );
565
+ err_alloc:
566
+	return rc;
567
+}
568
+
569
+/** EFI local file URI opener */
570
+struct uri_opener efi_local_uri_opener __uri_opener = {
571
+	.scheme	= "file",
572
+	.open	= efi_local_open,
573
+};

Loading…
Cancel
Save